Apollo Server + TypeScriptで楽しいスキーマ駆動開発

author potpro(ぼとぷろ)
2020/06/30

Apollo Server + Nuxt + TypeScriptで楽しいスキーマ駆動開発

最近GraphQLを本格的に調べていて、結論この構成が一番シンプルで楽しいかな(個人的に)ということで書きました。

この手法で、今までの「APIサーバーが出来るのを待つ」「途中での仕様変更がフロント・バックエンド間で共有されてない」みたいな問題をApollo Serverは解決でき、なおかつシンプルかつ標準機能として搭載されているため、開発が高速化出来そう・・・という気持ちです。

問題としてはGraphQLの知識が前提となりますが、そこに関しては学習コストが必要ですね。そこさえ乗り切ればかなり効率的な開発が出来ると考えればいいと思います。

なお、この記事はGraphQLの基礎のschemaやresolverなどの説明はあまりしませんので、そのあたりは知識がある前提の話になっています。

スキーマ駆動開発

Schema Driven Development(スキーマ駆動開発)。ここではチームでのWeb API開発フローに関する開発手法のことです。 Web APIは大規模になるほどかなりしづらく、Webの開発現場の中では多くのチームが関わると非常に共有や柔軟な変更が難しく、コミュニケーション不足による事故につながります。

つまりは、「こんな仕様私知らない!」「stringで帰ってくるって言ったじゃないですかー」という話が良く起きるという話です。

特に1つのAPIを多くの別のチームが使用する、なんて状態だと毎回チームに関していろんな質問が飛んできたりドキュメントの整備を疎かには出来ません。

また、スキーマが存在すれば、そのスキーマから自動生成し、モックサーバーを立てる、なんてことも可能です。

これを行うことで、まだ出来ていないAPIの部分をモックで返すことで、API利用者側の開発者は開発を進めることができます。

同時並行で開発出来ないとかなり開発効率は落ちると思われますので、モックサーバーがあるとかなりありがたいです。

Swagger(RestAPI)とGraphQL

Web API開発フローなので、Rest APIを使用してのスキーマ駆動開発も存在しますが、正直なところこれはやり辛いです。というのも、通常Rest APIには、スキーマというものが存在しないから。

Swagger(OpenAPI)が現在は事実上のRest APIにおけるスキーマのデファクトスタンダートですが、Swagger自体はAPI仕様書でしかないため、スキーマからコードを生成したり、モックを生成したり、という機能はいろいろなツールを組み合わせて行うこととなります。

対するGraphQL(サーバー)はスキーマを定義してから開発を行うこととなるためスキーマ駆動開発は個人的にはベストの開発手法だと感じており、周辺のツールもGraphiQLやApollo Clientなど、標準で揃っているあたりで、GraphQLの方が優れているかなと感じました。

今回はApollo Serverを使用するので、GraphQLを使用したスキーマ駆動開発の話となります。

(というよりも、GraphQLに慣れるとRest APIは大量のエンドポイントを生やしたり、バリデートの処理などが本当に大変だなあと気持ちが生まれてきました。ここに関しては実際、使うツールにもよるんでしょうけどね・・・)

Apollo Server

Apollo ServerはオープンソースのNode.js用GraphQLサーバです。

シンプルさ、パフォーマンス、コミュニティを重視しており、特徴としてはシンプルかつ多機能でほとんどのNode.js製フレームワークをサポートしているところです。

多くのスターを獲得してますし、フレームワークとしてはかなり成熟していると思います。

Apollo Serverはnpmよりインストール後、1つのjsファイルですぐに起動することができます。

これもシンプルかつ非常に簡単で、学習用としても本番用としても使いやすいです。

以下は公式ドキュメントに書いてあるサンプルです。

npm install apollo-server graphql

main.js

const { ApolloServer, gql } = require('apollo-server');

// The GraphQL schema
const typeDefs = gql`
  type Query {
    "A simple type for getting started!"
    hello: String
  }
`;

// A map of functions which return data for the schema.
const resolvers = {
  Query: {
    hello: () => 'world',
  },
};

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

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

今回は、このJSファイルをベースとして開発します。今回はTypeScriptなのでこれをそのままts拡張子に変えて、ts-nodeを使用しています。

スキーマの作成

スキーマを作成します。今回はとりあえず商品データを返却するQueryとTypeみたいなのを作ります。

GraphQLファイルは、gqlという拡張子で保存します(graphqlという拡張子も見かけるので、厳密に決まってるわけではない?)。

query.gql

type Query {
    product(id: String): Product
}

type Product {
    id: ID!
    name: String!
    brand: Brand!
    url: String
    image_url: String
    reviewer_average: Float!
    review_count: Int!
}

type Brand {
    id: ID!
    url: String
    name: String!
}

このGraphQLファイルを読み込むには、graphql-importというライブラリを使うことで読み込みことができます。

このライブラリを使うことで、JSファイルとGraphQLスキーマをファイルで分割することが可能となります。

const { ApolloServer, gql } = require('apollo-server');
const { importSchema } = require('graphql-import');

// The GraphQL schema
const typeDefs = importSchema('schema.gql');

// A map of functions which return data for the schema.
const resolvers = {
  Query: {
    hello: () => 'world',
  },
};

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

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

これでスキーマはとりあえず完成です。簡単です。

graphql-codegenでresolverの型生成

graphql-codegenはGraphQLファイルより、TypeScriptの型をサーバ向けに自動生成してくれるツールです。

使い方は割と簡単。

npx @graphql-codegen/cli init

で導入後、codegen.ymlという設定ファイルをルートディレクトリに用意します。

overwrite: true
documents: null
schema: 'schema.gql'
generates:
    generated/graphql.ts:
        plugins:
            - 'typescript'
            - 'typescript-operations'
            - 'typescript-resolvers'
        config:
            useIndexSignature: true
            avoidOptionals: true

このコマンドを叩くと、generated/graphql.tsという型定義ファイルが出来るはずです。

graphql-codegen

後はこの型を参考にして、QueryやResolverを実装していくだけでOK。 詳しくは書きませんが、こんな感じです。

とりあえず{} as Productにして型だけ通るようにしています。 (requireな値が含まれているので、実際は実行時にエラー吐きますけど)

resolver.ts

import { QueryResolvers, Product } from '../generated/graphql';
const Query: QueryResolvers = {
    // ここが実装部分
    product: async (_parent, args, _context, _info) => ({} as Product),
};

export const resolvers = {
    Query,
};

TSの書き方に合わせるようmain.tsを少し変更。

import { ApolloServer, gql } from 'apollo-server';
import { importSchema } from 'graphql-import';
import { resolvers } from 'resolver.ts';

// The GraphQL schema
const typeDefs = importSchema('schema.gql');

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

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

すごい端折ってますが、これでresolverの引数と返り値に関しては、TypeScriptの型定義が利くようになっているはずです。

モックサーバー化

実装部分がまだできていませんが、このまま実装は作らずに、モックサーバとして実行し、使用できるようにします。

Apollo Serverをモックサーバー化する方法は非常に簡単です。Apollo Serverの引数にmocksというものを入れるだけ。

const server = new ApolloServer({
  typeDefs,
  resolvers,
  mocks: true,
});

こうして起動後、GraphiQLから叩いてみるとこんな感じでモックが動作します。

graphql-mock

型に合わせて既定の文字列が入る感じです。

しかし、自分で特定の値を返したいという場合もあるかもしれません。そういう場合でも簡単に設定することが可能です。

mocksの値をそのまま配列で渡すことで、値を設定することができます。

const server = new ApolloServer({
    typeDefs,
    resolvers,
    mocks: {
        Product: () => ({
            id: '100200300',
            name: '製品名',
            url: 'https://example.com',
            image_url: 'https://blog.potproject.net/noimg.png',
            reviewer_average: 5.0,
            review_count: 10,
        }),
        Brand: () => ({
            id: '100',
            url: 'https://example.com',
            name: 'ブランド',
        }),
    },
});

配列の場合は、mocklistを使用して返す数などを設定することもできます。そのあたりを詳しく知りたい場合はMocking - Apollo Serverを参照。

このモック化したサーバーはそのままGraphQLエンドポイントとして動作するので、そのままフロントエンドの開発用エンドポイントとして使用すれば、スムーズにAPI側の実装と並行して開発を進めることができると思います。

スキーマ駆動開発どうよ?

ソロで開発しているのであればいいですが、実際は仕事上で「APIサーバーがないからアプリが作れない!早く作って」みたいな要望はあります。

今のマイクロサービス的なアプローチの場合、機能ごとにAPIを切り分ける必要が出る場面も多く、その部分の開発スピードが非常にネックです。

以前はこちらでモックサーバーを提供することによりその辺りもシンプルに解決することができそうなことがわかると思います。

スキーマ駆動開発はGraphQL以外にも、gRPCなどもスキーマを定義して開発する仕組みのため、採用しやすく、次世代の開発に適した開発手法ではないかと思います。