🚀

Apollo Server with TypeScript

2021/06/26に公開1

はじめに

Apollo Severを使う機会があったのでTypeScriptのための手順をまとめてみます。

ツール、実行環境のバージョン情報

  • node: v16.0.0
  • npm: 7.14.0
  • git: 2.32.0

デモアプリケーションの構築

リポジトリの初期生成

リポジトリの初期セットアップを行います。gts はGoogle TypeScript StyleというGoogle流のTypeScriptのガイドラインをESLintとPrettierの設定としてセットアップしてくれるツールです。このスタイルに従う必要はありますがTypeScript版のnpm init -yとして使うことができて便利なので、今回はこれを使ってセットアップします。

mkdir apollo-server-with-typescript
cd apollo-server-with-typescript
npx gts init -y

package.jsonを開きnameを設定します。

{
  "name": "apollo-server-with-typescript",
  ...
}

Git管理しながら進めたいので、.gitignoreを作成します。

touch .gitignore
node_modules
build

初期コミットします。

git add .
git commit -m "🎉 initial commit"

サンプルデータに対するQueryの実装

必要なライブラリをインストールします。

npm install apollo-server graphql 
npm install --save-dev ts-node ts-node-dev

src/index.tsの既存の内容を削除し、新たに記述していきます。

import {ApolloServer, gql} from 'apollo-server';

// GraphQLスキーマの定義
const typeDefs = gql`
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book!]!
  }
`;

// サンプルデータの定義
const books = [
  {
    title: 'The Awakening',
    author: 'Kate Chopin',
  },
  {
    title: 'City of Glass',
    author: 'Paul Auster',
  },
];

// リゾルバーの定義
const resolvers = {
  Query: {
    books: () => books,
  },
};

// サーバーの起動
const server = new ApolloServer({typeDefs, resolvers});

server.listen().then(({url}) => {
  console.log(`🚀  Server ready at ${url}`);
});

このコードはApollo Serverの公式チュートリアルをTypeScript化したものです。

package.jsonに開発/本番用のサーバー起動コマンドをに下記のように追記します。

...
  "scripts": {
    "dev": "ts-node-dev --respawn src/index.ts",
    "start": "node build/src/index.js"
    ...

tsconfig.jsonに設定を下記のように追記します。

...
  "compilerOptions": {
    "esModuleInterop": true,
    ...

開発用サーバーを立ち上げます。

npm run dev

クエリを投げるためにApollo Sandboxを利用します。ブラウザで開きクエリを入力してRunボタンを押します。

query {
  books {
    author
    title
  }
}

Apollo Sandboxでのクエリ実行

レスポンスで下記のJSONが受け取ることができれば、正しく実装ができています。

{
  "data": {
    "books": [
      {
        "author": "Kate Chopin",
        "title": "The Awakening"
      },
      {
        "author": "Paul Auster",
        "title": "City of Glass"
      }
    ]
  }
}

ここまでの変更をコミットします。

git add .
git commit -m "✨ add sample quer"

Schemaの外部ファイル化

Schemaはフロントエンドとも共有する必要があるので、プロダクション利用ではファイルに切り出す必要があります。
プロジェクトディレクトリのルートにschema.graphqlを作成し、src/index.tsからconst typeDefs = ...の定義を削除します。

# schema.graphql
type Book {
    title: String
    author: String
}

type Query {
    books: [Book!]!
}

SchemaをApollo Severに取り込ませるためのライブラリをインストールします。調べるとgraphql-importを使っている情報が多く出てきますが、既に利用非推奨となっているのでGraphQL Toolsを使います。

npm install @graphql-tools/load \
            @graphql-tools/schema \
            @graphql-tools/graphql-file-loader

src/index.tsを下記の様に編集します。

import {ApolloServer} from 'apollo-server';
import {loadSchemaSync} from '@graphql-tools/load';
import {GraphQLFileLoader} from '@graphql-tools/graphql-file-loader';
import {addResolversToSchema} from '@graphql-tools/schema';
import {join} from 'path';

// サンプルデータの定義
const books = [
  {
    title: 'The Awakening',
    author: 'Kate Chopin',
  },
  {
    title: 'City of Glass',
    author: 'Paul Auster',
  },
];

// スキーマの定義
const schema = loadSchemaSync(join(__dirname, '../schema.graphql'), {
  loaders: [new GraphQLFileLoader()],
});

// リゾルバーの定義
const resolvers = {
  Query: {
    books: () => books,
  },
};

const schemaWithResolvers = addResolversToSchema({schema, resolvers});

// サーバーの起動
const server = new ApolloServer({schema: schemaWithResolvers});

server.listen().then(({url}) => {
  console.log(`🚀  Server ready at ${url}`);
});

先程と同様に開発用にサーバーを立ち上げApollo Sandboxでクエリが実行できるか確認します。

ここまでの変更をコミットします。

git add .
git commit -m "♻️ cut out the schema to an external file"

SchemaからTypeScriptの為に型を生成する

GraphQL Code Generatorを用いてTypeScript用の型を生成することで、型のサポートを受けてリゾルバーを書くことができます。

ツールをインストールします。

npm install --save-dev \
            @graphql-codegen/cli \
            @graphql-codegen/typescript \
            @graphql-codegen/typescript-resolvers \

プロジェクトディレクトリのルートに型生成のための設定をcodegen.ymlとして作成します。

overwrite: true
generates:
  ./src/types/generated/graphql.ts:
    schema: schema.graphql
    config:
      useIndexSignature: true
      # リゾルバーのためのContextの型をsrc/types/context.d.tsから読み込む
      contextType: ../context#Context
    plugins:
      - typescript
      - typescript-resolvers

Contextの型をsrc/types/context.d.tsに定義します。今回はユーザーに名前、メールアドレス、トークンがContextに保持されものとします。プロダクション利用ではDBに対してアクセスするためのクライアントかそれをより抽象化したインスタンスを保持する形になりそうです。

export type Context = {
  user?: {
    name: string;
    email: string;
    token: string;
  };
};

package.jsonに型を生成するコマンドをに下記のように追記します。

...
  "scripts": {
    "codegen": "graphql-codegen --config codegen.yml",
    ...

型を生成します。

npm run codegen

生成された型を使用しsrc/index.tsを編集します。const resolvers: Resolvers = {...}として型を宣言することでリゾルバー関数は引数や返り値などで型のサポートを受けることができます。

HTTPリクエストのAuthorizationヘッダーにユーザーのトークンが乗っていると想定してContextを定義します。全てのエンドポイントがプライベートであると想定し、トークンが乗っていない場合はエラーを送出します。パブリックなエンドポイントも含む場合はリゾルバー側でエラーを送出しましょう。

import {ApolloServer, AuthenticationError} from 'apollo-server';
import {loadSchemaSync} from '@graphql-tools/load';
import {GraphQLFileLoader} from '@graphql-tools/graphql-file-loader';
import {addResolversToSchema} from '@graphql-tools/schema';
import {join} from 'path';
import {Resolvers} from './types/generated/graphql';
import {Context} from './types/context';

// サンプルデータの定義
const books = [
  {
    title: 'The Awakening',
    author: 'Kate Chopin',
  },
  {
    title: 'City of Glass',
    author: 'Paul Auster',
  },
];

// スキーマの定義
const schema = loadSchemaSync(join(__dirname, '../schema.graphql'), {
  loaders: [new GraphQLFileLoader()],
});

// リゾルバーの定義 (型のサポートを受けれる)
const resolvers: Resolvers = {
  Query: {
    books: (_parent, _args, _context) => {
      // TODO: 詳細な認可処理を行う

      return books;
    },
  },
};

const schemaWithResolvers = addResolversToSchema({schema, resolvers});

const getUser = (token?: string): Context['user'] => {
  if (token === undefined) {
    throw new AuthenticationError(
      '認証されていないユーザーはリソースにアクセスできません'
    );
  }

  // TODO: Tokenからユーザー情報を取り出す処理

  return {
    name: 'dummy name',
    email: 'dummy@example.com',
    token,
  };
};

// サーバーの起動
const server = new ApolloServer({
  schema: schemaWithResolvers,
  context: ({req}) =>
    ({
      user: getUser(req.headers.authorization),
    } as Context),
  debug: false, // エラーレスポンスにスタックトレースを含ませない、開発環境ではtrueにした方が分析が捗りそう
});

server.listen().then(({url}) => {
  console.log(`🚀  Server ready at ${url}`);
});

Apollo SandboxでAuthorizationヘッダーを付与した状態でクエリを実行し成功、付与しない状態で失敗すれば成功です。

Apollo Sandboxでのクエリ実行(成功)
Apollo Sandboxでのクエリ実行(失敗)

最後にコミットします。

git add .
git commit -m "♻️ generates types for TS"

あとがき

公式のドキュメントがJavaScript向けの情報のみでTypeScript向けの情報がほぼ無かったので、調べながらまとめてみました。

参考元

Discussion

じゅんじゅん

とてもわかりやすく開発を始めるにあたって参考になりました!
素敵な記事をありがとうございます!