16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-18

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をすれば良いです。

16
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?