GraphQL APIをスキーマファースト開発するためのモックサーバをRailsとApolloで作る

GMOペパボ Advent Calendar 2017の23日目の記事です。

今回はJavaScriptでGraphQLのサーバ/クライアントや関連ツールを提供しているApolloのツールセットでRailsプロジェクトでGraphQLのモックサーバを立ち上げるところまでを試してみます。

業務でRails製の(RESTishな)Web APIとVue.js製のSPAからなるアプリケーションを開発していて、スキーマファースト開発を取り入れています。また、GraphQLで通信するAPIを実験的に導入しはじめていますが、こちらは明示的な開発フローを決めず導入しようとしているため、なかなかサクサクと開発が進まないのが現状です。そこで、GraphQLでも先にインタフェースだけを決めてから、モックサーバを使ってフロントエンドとバックエンドで並行開発していけばよいのでは、という発想になります。

しかし、そもそもGraphQLはサーバに対するクエリを書くためのスキーマありきの技術であり、それがRESTの文脈におけるAPIとは異なる点です。その点で、スキーマファースト開発と呼ぶと語弊があるかもしれません。ですが、ここでは「GraphQLの型やフィールドだけを書いて、実際にデータを問い合わせる部分(リゾルバ)を書かない」ことをスキーマファースト開発とひとまず呼びます。つまり、裏の実装を後回しにして、フロントエンド/バックエンドでインタフェースについて合意が取れればモックサーバを使って開発を進められる、という状態を目指します。

利用ツール

上述したとおり、Apolloのツールセットを使います。

www.apollographql.com

具体的には次のものを使います。

ダミーデータを返してくれるサーバのことをスタブサーバと呼んだりもしますが、graphql-toolsが"Mocking"という言葉を使っているので、この記事ではモックサーバと呼ぶことにします。

最終構成

今回はRailsでgraphql-rubyを使っている状況を想定します。Railsプロジェクトにおける最終的な構成は次のとおりです(関係する部分だけ書いています)。

.
├── app
│    └── graphql
│         ├── app_schema.rb
│         ├── mutations
│         └── types
├── lib
│    └── tasks
│         └── graphql.rake
└── mock_app
     ├── index.js
     ├── mocks.js
     ├── package.json
     └── type_defs.js

詳細は次の通りです。

  • app 配下にgraphql-rubyで書いたGraphQLスキーマを置く
  • lib 配下にGraphQLスキーマをダンプするRakeタスクを置く
  • mock_app 配下にモックサーバの実装を置く
    • type_defs.js はRakeタスクで生成する

想定する開発フロー

想定する開発フローは次の通りです。

  1. graphql-rubyのDSLでGraphQLの型やフィールドを書く
  2. 追加した型やフィールドのダミーデータを書く
    • モックサーバで使う
  3. レビュー
  4. モックサーバを立ち上げる
    • graphql-rubyでGraphQLスキーマをダンプしてApolloで使える形式にする
    • ExpressとApolloでGraphQLモックサーバを立ち上げる
  5. フロントエンド/バックエンドが並行して開発する

それぞれ説明します。

graphql-rubyのDSLでGraphQLの型やフィールドを書く

これはgraphql-rubyをふつうに使うときとほぼ同じになります。まだ裏の実装ができていないので resolver を書かない点が違いといえます。

Types::QueryType = GraphQL::ObjectType.define do
  name 'Query'

  field :user do
    type Types::UserType
    argument :email, !types.String

    resolve ->(obj, args, ctx) {
      # まだ裏の実装がないので書かない
    }
  end
end

Types::UserType = GraphQL::ObjectType.define do
  name 'User'

  field :email, !types.String
  connection :articles, Types::ArticleType.connection_type
end

Types::ArticleType = GraphQL::ObjectType.define do
  name 'Article'

  field :title, !types.String
  field :body, !types.String
end

このように開発に必要な型とフィールドだけを書いていきます。

追加した型やフィールドのダミーデータを書く

モックサーバとして動かすには、サーバになんらかのデータを返してもらう必要があります。Apolloのgraphql-toolsで作れるモックサーバは、フィールドの型に応じてある程度ランダムにデータを返してくれるようになっています。しかし、実際に返ってくるであろうものに近いデータを返したほうがフロントエンドの開発ではありがたいということもあるでしょう。また、ダミーデータを見ればフィールドの表現しているものの雰囲気がわかるという利点もあります。

Apolloのモックサーバが返す値を指定するために、次のようなオブジェクトを定義します。ここでは仮に mocks.js とします。

// mock_app/mocks.js

module.exports = {
  User: () => ({
    email: 'kymmt90@example.com',
  }),
  Article: () => ({
    title: 'The Article',
    body: 'This is the article.',
  }),
};

GraphQLの型に対して、型のフィールドとダミーデータを持つオブジェクトを書き、それを返す関数を持たせているだけです。これを書いておくだけで、connectionなどを使ってクエリがネストしているときも、graphql-toolsのモックサーバがいい感じにダミーデータを返してくれるようになります。

レビュー

上述した流れでスキーマとダミーデータだけ書けたら、チームでレビューするなりして合意をとります。

モックサーバを立ち上げる

Apolloを使ってモックサーバを立ち上げます。

Expressとapollo-serverを使って、次のようなサーバを書きます。ここでは mock_app/index.js とします。

// mock_app/index.js

const express = require('express');
const bodyParser = require('body-parser');
const { graphqlExpress } = require('apollo-server-express');
const { addMockFunctionsToSchema, makeExecutableSchema } = require('graphql-tools');

// モックサーバの作成
const typeDefs = require('./type_defs');
const schema = makeExecutableSchema({ typeDefs });
const mocks = require('./mocks')
addMockFunctionsToSchema({ schema, mocks });

// GraphQLエンドポイントを持つExpressサーバの立ち上げ
const app = express();
app.use('/graphql', bodyParser.json(), graphqlExpress({ schema }));
app.listen(3000, () => {
  console.log('GraphQL mock server is running!!1');
});

mock_app/package.json は次のような感じです。

{
  "name": "graphql-mock-server",
  "private": true,
  "version": "0.0.1",
  "description": "graphql-mock-server",
  "author": "kymmt90",
  "dependencies": {
    "apollo-server-express": "*",
    "graphql-tools": "*",
    "graphql": "*",
    "express": "*",
    "body-parser": "*"
  }
}

index.js に書いたように、GraphQLスキーマを type_defs.js から読み込みます。この type_defs.js を得るために、graphql-rubyで定義したスキーマをもとに、次のRakeタスクを書き、スキーマをダンプできるようにします。

# lib/tasks/graphql.rake
namespace :graphql do
  namespace :schema do
    desc 'Dump GraphQL schema as a JavaScript file'
    task dump_as_js: :environment do
      schema = AppSchema.to_definition
      File.open(Rails.root.join('mock_app', 'type_defs.js'), 'w') do |f|
        f.puts("module.exports = `\n")
        f.puts(schema)
        f.puts('`')
      end
    end
  end
end

bin/rails graphql:schema:dump_as_js を実行すると次のようなファイルが得られます。type_defs.js では、GraphQLスキーマをJSの文字列として定義しています。

// mock_app/type_defs.js

module.exports = `
type Article {
  body: String!
  title: String!
}

# The connection type for Article.
type ArticleConnection {
  # A list of edges.
  edges: [ArticleEdge]

  # Information to aid in pagination.
  pageInfo: PageInfo!
}

# An edge in a connection.
type ArticleEdge {
  # A cursor for use in pagination.
  cursor: String!

  # The item at the end of the edge.
  node: Article
}

# Properties for creating an article by a specified user
input ArticleInputType {
  # Body of the article
  body: String

  # Title of the article
  title: String!

  # Email address of the user
  user_email: String!
}

type Mutation {
  # Create an article by the specified user
  createArticle(article: ArticleInputType): Article
}

pp# Information about pagination in a connection.
type PageInfo {
  # When paginating forwards, the cursor to continue.
  endCursor: String

  # When paginating forwards, are there more items?
  hasNextPage: Boolean!

  # When paginating backwards, are there more items?
  hasPreviousPage: Boolean!

  # When paginating backwards, the cursor to continue.
  startCursor: String
}

type Query {
  user(email: String!): User
}

type User {
  articles(
    # Returns the elements in the list that come after the specified global ID.
    after: String

    # Returns the elements in the list that come before the specified global ID.
    before: String

    # Returns the first _n_ elements from the list.
    first: Int

    # Returns the last _n_ elements from the list.
    last: Int
  ): ArticleConnection
  email: String!
}
`

ここまで来れば、あとは index.js をサーバとして起動すれば終わりです。

$ (cd mock_app && npm install && node start index)
GraphQL mock server is running!!1

次のように、サーバがGraphQLのクエリを受け取りつつ、自分で書いたダミーデータがサーバから返ってくるようになります。これでスタブサーバが手に入ったので、フロントエンドとバックエンドを並行開発していくことができます。

f:id:kymmt90:20171224113344p:plain

まとめ

Railsでgraphql-rubyを使っている場合に、Apolloのツールセットを使ってGraphQLのモックサーバを作る方法について説明しました。Apolloが便利なので、わりと簡単にセットアップできました。

GraphQL APIの開発方法はまだ模索段階なので、2018年はこれを業務に取り入れてみて気になる点がないか確かめていきたいという気持ちです。

参考