Help us understand the problem. What is going on with this article?

Apollo serverをTypescriptで安全に開発する

Apollo serverを書くにあたって、ドキュメントがほとんどjavascriptでtypescriptで書くのがちょっと手こずったので覚書。

GraphQL Codegen

GraphQLの魅力はなんといってもそのスキーマの堅実さにありますよね。その堅実さをTypescriptの型定義にあやかろうというのがGraphQL Codegenです。

まずはnpmから必要なモジュールを取ってきてください

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

SDLで記述されたschema.graphqlが作成できたら、ルートフォルダあたりにcodegen.ymlを記述してください

codegen.yml
overwrite: true
documents: null
schema: "**/*.graphql"
generates:
  src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-resolvers"
    config:
      useIndexSignature: true
      avoidOptionals: true

src/generated/graphql.tsにいろいろな型が生成されます。後述するApollo特有のものもありますが、普通のtypeやinput typeをそのまま型にしたものもあるので有効活用できます。

エントリポイントの作成

ほぼチュートリアルと一緒ですが、index.tsを作成します。

index.ts
import { ApolloServer, gql } from 'apollo-server';
import fs from 'fs';
import path from 'path';

import resolvers from './resolvers'; //後で定義
import { dataSources, context } from './datasources'; //後で定義


const typeDefs = gql(fs.readFileSync(path.resolve(__dirname, '<スキーマファイルへのパス>'), 'utf8'));

const server = new ApolloServer({
  typeDefs,
  resolvers,
  dataSources,
  context
});

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

typeDefsの定義がエレガントじゃない気がしますが...もっといい方法募集してます。

Datasource, Contextの定義

とりあえず、contextを定義します。

datasources.ts
export const context = async () => ({
  /* Contextの定義 */
  ctx1: "foo"
  ctx2: "bar"
});

type PartialContext = ReturnType<typeof context> extends Promise<infer T> ? T : never;

Typescriptのinferについては、私の過去の記事を参照してください❤️

次にDataSourcesを定義します。

datasources.ts
export const dataSources = () => ({
  /* Datasourceの定義 */
  ds1: new MyDataSource1<PartialContext>(),
  ds2: new MyDataSource2<PartialContext>()
});

なんでDataSourceがジェネリクスを利用しているのかは後述いたします。実際に使われるContextの方は、PartialContextにdatasourceを加えたものになります。

datasources.ts
export type TContext = PartialContext & {
  dataSources: ReturnType<typeof dataSources>
};

自分のDatasourceを追加する

こんな感じでやります。

MyDatasource1.ts
import { DataSource, DataSourceConfig } from 'apollo-datasource';
import { InMemoryLRUCache, KeyValueCache } from 'apollo-server-caching';

export interface Context {
  ctx1: string
}

export default class MyDataSource1<T extends Context> extends DataSource<T> {

  context?: Context
  cache?: KeyValueCache

  constructor() {
    super();
  }

  initialize({ context, cache }: DataSourceConfig<Context>) {
    this.context = context;
    // this.context.ctx1にアクセスできる
    this.cache = cache || new InMemoryLRUCache();
  }
}

ジェネリクスを使ったのは、DataSourceに必要なContextがPartialContextと一貫性があることを保証するためです。

Resolverの定義

resolver.ts
import { TContext } from './datasources';
import { Resolvers } from './generated/graphql';

export const resolvers: Resolvers<TContext> = {
   // resolverのなかみ
};

やっとgraphql-codegenの強みが出てきました。ResolversとTContextによって、resolver関数の引数の型が推論されるようになります。resolverを分解してファイルをコンパクトにすることもできます。

resolver.ts
import { TContext } from './datasources';
import { Resolvers, QueryResolvers, UserResovlers } from './generated/graphql';

const queryResolvers: QueryResolvers<TContext> = {...}
const userResolvers: UserResolvers<TContext> = {...}

export const resolvers: Resolvers<TContext> = {
  Query: queryResolvers,
  User: userResolvers 
};

Scalarの定義

そもそもscalarの仕様がわかりにくすぎてヤバイ感じでした。

こんな感じです

datetime.ts
import {
  GraphQLScalarType,
  Kind,
  GraphQLScalarTypeConfig,
  ValueNode
} from 'graphql';
import moment from 'moment';

const config: GraphQLScalarTypeConfig<moment.Moment, string> = {
  name: 'DateTime',
  description: 'DateTime custom scalar type, in the form of YYYY-MM-DDTHH:mm:ss',
  parseValue(value: string) {
    return moment(value);
  },
  serialize(value: moment.Moment) {
    return value.format();
  },
  parseLiteral(ast: ValueNode) {
    switch (ast.kind) {
      case Kind.STRING:
        return moment(ast.value);
      default:
        return null;
    }
  }
};

export const datetimeScalar = () => new GraphQLScalarType(config);

GraphQLScalarTypeConfig<moment.Moment, string>は、SDLではstring型であり、resolverの第二引数ではMoment型であるScalar型であることを示しています。parseValueはSDL→resolver、serializeはresolver→SDLへの変換となります。

parseLiteral何に使うんですかね?ようわからんです。

Directiveの定義

まだ使ったことないのでわからんとです

Federationの利用

Apolloを使う理由の一つが、federationによるマイクロサービスの定義のしやすさかもしれません。

まず、codegen.ymlに次を追加します

codegen.yml
generates:
  src/generated/graphql.ts:
    plugins:
      ...
    config:
      avoidOptionals: true
      useIndexSignature: true
      federation: true #追加

あとはチュートリアルの通りにschemaをすれば良いです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした