開発

スキーマ駆動開発 vol.2 WebAPI開発「OprnAPIによるスキーマ駆動開発」「GraphQLによるスキーマ駆動開発」

WEB+DB Press vol.108」で紹介されていたスキーマ駆動開発の続きになります。今回は実際に匿名掲示板にて作成していき、より具体的にアプローチしていきます。

OprnAPIによるスキーマ駆動開発 匿名掲示板用APIをサンプルに

REST APIのスキーマ駆動開発について具体的に見ていく。
こちらの記事で紹介した、匿名掲示板用APIをREST APIとして開発し、クライアントから利用するという状況を想定。

スキーマ駆動開発の開発フローは下記になる。

  • 1.実装する機能で必要になるAPIエンドポイントのスキーマを設計する
  • 2.チーム内でスキーマについてレビューする
  • 3.スキーマに従ってAPIサーバーとクライアントを平行作業で実装する
  • 4.APIサーバーとクライアントを結合する

クライアントとAPIサーバーの開発方法

匿名掲示板APIをREST開発 → クライアントから利用する という状況を想定。

クライアント

クライアントの開発ではスキーマ(本記事でのOpenAPI ドキュメント)とOpenAPI Generatorを利用する。
OpenAPI Generatorを使用し、スキーマからクライアントのコードとスタブサーバーを生成する。

本物のAPIサーバーの実装が完了するまではスタブサーバーを利用して開発を進める。
APIエンドポイントへのアクセスには生成したAPIクライアントを利用することで実装工数を減らすことができる。

APIサーバー

APIサーバーの開発においてはスキーマと実装の乖離を防ぐことが重要
スキーマに定義したリソースのSchemaオブジェクトを利用して、スキーマに準拠するAPIエンドポイントを実装する。

そうすることでスキーマと実際のレスポンスの間に乖離がないかを検証することができる。

クライアント側の開発を実装(駆動)

こちらの記事では、OpenAPI Generotorを使用している。
上記記事にてSwagger CodegenではなくOpenAP Generotorを用いているのは執筆時点でSwagger CodegenはOpenAPI Specification バージョン3に対応していなかったから。

※Swagger Codegenは2019.04.25にOpenAPI Specification バージョン3に対応。

OpenAPI Generotorって?

Web APIのスキーマからAPIクライアントやスタブサーバーを生成できるツール。
OpenAPI GenerotorはAPIクライアントとスタブサーバーそれぞれにおいて各言語のHTTPクライアントライブラリやWebアプリケーションフレームワークの種類を指定することができる。

OpenAPI GenerotorはSwagger Codegenよりフォークしたプロジェクト。

まずはスキーマからAPIクライアントを生成

このAPIクライアントは次のような機能をもつ。

  • APIの各エンドポイントに対応するメソッドをもつ
  • APIリクエスト用のオブジェクトからスキーマの中で定義したJSONのキーの型を考慮しリクエストボディへ変換する
  • APIがレスポンスとして返すJSONを、スキーマ中で定義したJSONのキーの型を考慮してオブジェクトへ変換する

OpenAPI Generatorはスキーマ上で定義した属性の型に基づいてクライアントライブラリを生成することができる。
具体的にはAPIクライアントの利用者がオブジェクトをもとにリクエストを送ったりレスポンスを受け取ったりするときにAPIクライアントがJSONとオブジェクトの間のマッピングを担ってくれる

これにより仮にスキーマの定義に変更があっても、APIクライアントをふたたび生成すれば新しい定義のスキーマに対応できることがわかる。

実際にAPIクライアントを生成する その前に

ここでは
– OpenAPI Generator3.3.4
– 公式のDockerイメージ
を利用する。
あらかじめ、Docker Storeから使用するプラットフォームに対応したDocker Community Editionをインストールしておく。

いよいよ匿名掲示板API用・JavaScriptクライアントを生成する

下記コマンドを実行。

$ docker run --rm -v ${PWD}:/local \
openapitools/openapi-generator-cli generate \
-g javascript -DprojectName='anon-bbs-api' \
-i /local/openapi.yaml -o /local/javascript_client

生成したコードは下記のようになっている。

  • OpenAPI Generatorで生成したJavaScriptクライアント(一部)
javascript_client
|-READE.md
|-docs
|  |-(省略)
|-package.json
|-src
|  |-ApiClients.js
|  |-api
|  |  DefaultApi.js   
|  |
|  |-index.js
|  |  model
|  |  |-Comment.js
|     |-CommentParameters.js
|     |-CommentProperties.js
|     |-CommentRequest.js
|     |-Comments.js
|     |-ErrorProperties.js
|     |-Errors.js
|     |-Post.js
|     |-PostParameters.js
|     |-PostProperties.js
|     |-PostRequest.js
|       Posts.js
|_test
    -(省略)

APIクライアントの実体はsrc配下に配置される。
このクライアントライブラリをローカルで使えるようにする為、下記コードを記述。

$ cd javascript_client
$ npm link
$ cd ~/myapp ←自分のアプリケーションディレクトリへ移動する
$ npm link anon-bbs-api

APIクライアントは次のように使用。
~/myapp/index.js

var AnonBbsApi = require('annon-bbs-api');
var api = new AnonBbsApi.DefaultApi()
var postRequest = AnonBpsApi.PostRequest.constructFromObject(//ApiClientが持つスキーマで定義された型へ変換するためのメソッドを利用している
{
  post: {
  title: "こんには",
  content: "良い天気ですね"
  }
});
api.createPost( //APIエンドポイントPOST/postsを呼び出すためにメソッドcreatePostを使用している
  postRequest, //Postクラスのオブジェクトを渡している。OpenAPIにおけるcomponentに対応するModelクラスが生成されている為これを利用しリクエストボディとして使うPostクラスのオブジェクトを生成することができる
  function(error,data,response){
  if(error){
    console.error(error);
   } else { 
   console.log('response':' + response.text);
   }
}
);

「http://localhost:3000」 にてAPIサーバーを立ち上げ

クライアントのサンプルコードを実行

APIサーバーのエンドポイントPOST/postへリクエスト。

クライアントは投稿作成用のエンドポイントへ正常なパラメーターを送信している為、APIサーバー側は正常なレスポンスを返す

クライアントは成功時のメッセージを標準出力へ表示する。

$ node index.js
response: {"post":{"id":3,"title":"こんにちは","content":"良い天気ですね","posted_at":"2018-11-30T01:05:04Z"}}

スキーマからコードを生成することにより、APIサーバーとやりとりするデータの型とスキーマが定義している型とが乖離しないような仕組みになっている

次にスタブサーバーの生成

スタブサーバーの生成はAPIクライアントの生成とほぼ同じコマンドで行うことができる。
本記事ではJavaのSpring Boot1で稼働するスタブサーバーを生成する。

OpenAPI Generatorで生成したSpring Bootで動くスタブサーバーはスキーマの中のcomponetsに定義したSchemaオブジェクトのexampleフィールドが持つ値をレスポンスのJSONのキーが持つ値として利用することができる。

つまり、スキーマにダミーデータを書いておくことができるということ。

スタブサーバーを生成するコマンド

$ docker run -v {PWD}:/local \ 
openapitools/openapi-generator-cli generate \
-i /local/openapi.yaml -g spring  -o /local/spring_stub \
--additional-properties returnSucessCode=true

スタブサーバーを立ち上げ利用する

カレントディレクトリに作られたspring_subディレクトリへ移動

スタブサーバーを立ち上げるためのコマンドを実行

Dockerイメージのmavenとjavaを利用しSpring Bootアプリケーションを起ち上げる

$cd ./spring_stub
$docker run -- v ${PWD}:/usr/src/mymaven \ -w /usr/src/mymaven maven mvn package
$docker run --rm -p 3000:3000 \ -v ${PWD}:/usr/src/myapp -w /usr/src/myapp \java java -jar target/openapi-spring-1.0.0.jar

http://localhost:3000にスタブサーバーが起動する

スタブサーバーへHTTPリクエストを送信してみる

下記のようにダミーデータが返ってくる

$curl -s http://localhost:3000/posts |jq .
{
  "posts":[
  {
    "id": 1,
    "title": "こんにちは",
    "posted_at": "2000-01-23T04:56:07.000+00:00",
    "content":"今日は良い天気ですね"
  },
  {
    "id": 1,
    "title": "こんにちは",
    "posted_at": "2000-01-23T04:56:07.000+00:00",
    "content":"今日は良い天気ですね"
  }
]
}

このようにスキーマにAPIエンドポイントとそのレスポンスを定義しておくと簡単にスタブサーバーを立ち上げることができる
実際にのAPIサーバーが実装完了するまではこのスタブサーバーを使ってクライアントの開発を進めることができる。

スキーマを使ったAPIサーバーの開発

スキーマ駆動開発においては、スキーマはWeb APIの仕様についての一時情報となる。
APIサーバーを実装する際はそのレスポンスが含むデータ構造がスキーマ上のレスポンス定義と一致するよう気を付ける必要がある。

  • Postの持つフィールドを表すSchemaオブジェクト
type: object
properties:
  id:
    type: integer
    example: 1
  title: 
    type:string
    example: 良い天気ですね
  content:
    type: string
    example: 良い天気ですね
  posted_at:  
    type: string
    format: data-time
    example: '2018-12-01T00:00:00Z'
  required:
    -id
    -title
    -content
    -posted_at

上記を行う際、APIサーバーがPostについて次のようなレスポンスを返してしまうとキー名が違いクライアントがうまく値を取得できなかったり、取得した値の型が違ったりといった問題が発生する

  • Postを表すAPIサーバーのレスポンスJSON・間違った例
{
"id": "1"//intgerでなければいけないのにstringになってる
"title": "こんにちは"
"content": "良い天気ですね"
"submitted_at": "2018-12-01T00:00:00Z"//キー名が誤ってる
}

上記の問題を防ぐためにもスキーマを活用するとよい。
APIサーバーの自動テストでチェックする方法がおススメ。

CI(Continnous Integration、継続的インテグレーション)でテストを実行しておくことで、もし定義と実際のレスポンスの間に解離が発生したとしてもすぐに発見することができる。

実際にテストを行う方法について見てみる。
環境として、下記を用いる。

  • APIサーバー実装時のWebアプリケーションフレームワーク:Ruby on Rails 5.2.1.1
  • テスティングフレームワーク:23.8.0

まず、掲示板APIのスキーマで定義したエンドポイントPOST /post を次のように実装しているとする。

  • config/routes.rb
Rails.application.routes.draw do
 resource :posts, only: [:create]
end
  • app/contorollers/application_controller.rb
class ApplicationController < ActionController::API
end
  • app/contorollers/posts_controller.rb
class PostsController < ActionController::API
  # id、title、content、created_atをカラムとして持つテーブルpostsのレコードを保存する
  def create 
    @post = Post.new(post_params)

  if @post.save
    render status: :created 
  else
    # エラーのレンダリング
  end
end

private

  def post_params
    params.require(:post).permit(:title, :content)
  end
end
  • app/views/posts/create.json.jbuilder
json.post do
  json.(@post, id,title , :content)
  json.posted_at @post.create_at.iso8601
end

上記エンドポイントに対し、RSpecのrequest specは次のようになる。

  • spec/requests/posts_spec.rb
require 'rails_helper'

Rspec.describe 'Posts', type: :request do
  describe 'POST' /posts' do
    context 'when params are valid' do
    let(:code) { 201 }

    it 'creates a post and returns 201' do
      post '/posts'
        headers: {
          ACCEPT: 'application'/json'
          CONTENT_TYPE: 'application/json'
          }, 
        params: {
          post: {
            title: 'テストのタイトル',
            content: 'テストの本文'
            }
          }.to_json
       expect(response.status).to eq code 
       # !ここでAPIサーバーのレスポンス構造がスキーマと一致するかテストする!
       end
    end
  end
end

SchemaオブジェクトとJSON Schemaライブラリを用いてJSONのバリエーションを行う

まず、OpenAPIのSchemaオブジェクトを用いたJSONのバリデーション方法について解説。

スキーマからテストするエンドポイントに対応するOperationオブジェクトが持つResponseオブジェクトを取得する。
そして、対応するステータスコードのResponseオブジェクトが持つcontentフィールドの値であるSchemaオブジェクトを使用し、レスポンスに対するバリエーションを行う。

今回はOpenAPIのバージョン3に対応しているgemであるoas_parser0.14.0を用いてスキーマをパースする。

# スキーマをパースしてRubyのオブジェクトにする
spec = OasParser::Definition.resolve('openapi.yaml')
# POST /postの成功時(201)レスポンスを表すSchemaオブジェクトを取得
schema_data = spec.path_by_path('posts')
                .endpoint_by_method('post')
                .response_by_code('201')
                .content
                .dig('application/json', 'schema')

上記schema_dataはハッシュになっている

次に、3というgemとスキーマから得たSchemaオブジェクトを使用し実際のレスポンスがSchemオブジェクトに準拠しているのかバリデーションエラーを実行する

schema = JsonSchema.parse!(schema_data)
//このJSON Schemaインスタンスを利用し次のJsonSchema#validateメソッドで次のようにレスポンスのバリデーションを実行することができる

schema.validate({
  'post' => {
  'id'  => '1',
  'title'  => 'こんにちは', 
  'content' =>  '今日はくもりですね',
  'post_at' =>  '2018-12-01T15:00:00Z'
  }
})
=> [false,[#<JsonSchema::ValidationError: #/post/id: failed schema #/properties/post/properties/id: For 'properties/id', "1" is not an integer.>]]

//上記例はpostのキーの一つであるidの値の型がスキーマ上は整数である一方、JSONでは文字列型になっている為バリデーションエラーとなっRている

これまで準備した仕組みを用いると、RSpecのrequest specsを用いてAPIサーバーの実際のレスポンスを取得しそのレスポンスがスキーマ上のレスポンス定義と一致するかをテストできるようになる。

beforeブロックで実行しているSchemaオブジェクトの探索を各テストで実行できるようにヘルパメソッドにしておき、request specs実行前のフックとして実行できるようにしておく。

  • spec/support/request_specs_helper.rb
module RequestSpecsHelper
  def expect_to_conforms_schema(response)
    expect {
    @schema.validate!(JSON.parse(response.body))
    }.not_to raise_error
  end
end
  • spec/rails_helper.rb
#(省略)
RSpec.configure do |config|
#(省略)
  config.include RequestSpecsHelper
  config.before :example,type: :request do
    spec = OasParser::Difinition.resolve(
      Rails.root.join('public','openapi.yaml')
    )
   schema_data = spec.path_by_path(schema_path)
   .endpoint_by_method(schema_method)
   .response_by_code(code.to_s)
   .content
  #レスポンス定義が存在しないケース(204など)の対応として&.を利用
  &.dig('application/json', 'schema')
  @schema = JsonSchema.parse!(schema_data) if schema_data
  end
  end
  • spec/requests/posts_specs.rb
RSpec.describe 'Posts', type: :request do
 describe 'POST' /posts' do
  let(:schema_method) { 'post' }
  let(:schema_path) { '/post' }

  context 'when params are vaild' do
    let(:code) {201}

    it 'create a post' do
      post '/posts',
            headers: {
              ACCEPT: application/json'
              CONTENT_TYPE: 'application/json'
              },
             params: {
              post:
                title: 'テストのタイトル',
                content 'テストの本文'
             } 
            }.to_json
  expect(response.status).to eq code
  expect_to_conform_schema response
  end
  end
  end
end

ここまででスキーマを用いてスキーマを使用し、APIサーバーからのレスポンスがスキーマで期待する構造を持っているかを検証できるようになった。
以上の仕組みをいれておくと、エンドポイントのテスト追加時にスキーマと実装が乖離していないかどうかのチェックするテストを書けるようになる

実際にはこのような仕組みをもとに動くライブラリを用いてスキーマに基づいたAPIサーバーへの入出力のバリデーションを実行することが多い。

OpenAPIのSchemaオブジェクトを用いたバリデーションをより厳密にできる便利なフィールドとして下記のようなものがある。

  • additionalProperties:キーの存在可否をバリデーションする
  • required:必須プロパティをバリデーションする
  • nullable:値としてnullを許可するプロパティのバリデーション

Swagger UIを用いるとスキーマをもとにブラウザで参照できるWeb APIのリファレンスを生成し、そのリファレンス上でAPIサーバーへリクエストを送ることができる。

流れは下記。

  • Swagger UIを起動する
  • Swagger UIからAPIサーバーへアクセスするための設定を行う
  • Swagger UIでWeb APIの仕様をブラウザする

Swagger UIを用いるとインタラクティブなAPIリファレンスを手に入れることができ、スキーマの定義と一致した実装を持つAPIサーバーと組み合わせることで開発者がすぐAPIサーバーの概要を把握できるようになる。

OprnAPIによるスキーマ駆動開発 匿名掲示板用APIをサンプルに!のまとめ

チームで作り上げたスキーマを用いてAPIサーバーとそのクライアントの開発を駆動する方法について学んだ。
スキーマ駆動で開発を進めることでREST APIをサーバーとクライアントのインターフェイスとするアプリケーションの開発をこれまで以上に効率的に進めることができるだろう。

GraphQLによるスキーマ駆動開発~API仕様とクエリ言語を組み合わせて柔軟にデータを取得する~

GraphQLとは

Web APIのためのクエリ言語とその実行環境で構成されている。
GraphQLを用いたWeb APIはREST APIとは異なった形式のインターフェイスを持ち、また、仕様にスキーマという概念を含んでいる。

GraphQLを用いたWeb APIの実例

有名なのはGitHubのGraphQL API。
このAPIを利用してみる。

また、ここではGraphiQLというツールを使用する。
GraphiQLはクエリの編集が煩雑になりがちという問題をブラウザ上のGUI経由でGraphQL APIへクエリを送信できるように解決している

GraphiQLはGraphQLのAPIドキュメントの表示やクエリ補完にも対応している。

GraphQL APIへクエリを送信する流れ

ここで使用するのはElectronによるデスクトップアプリ版のGraphiQL.app0.7.2を用いる。

  • 1.あらかじめ自身のGitHubアカウントでpersonal acceess tokenを作成しておく。
  • 2.GraphiQLを起動する
  • 3.右上の「Edit HTTP Headers」をクリックする
  • 4.「+ Add Header」ボタンをクリックし、「Header name」をAuthorization、「Header value」をBearer<GitHubから取得したpersonal access token>の形式で入力し「Save」をクリックし保存する。
  • 5.「GraphQL Endpoint」に https://api.github.com/graphqlを入力し、GraphiQLの左側のエリアにクエリを入力する
  • 6.右向きの三角形のボタンをクリック

  • GitHubのGraphQL APIへ送信するクエリ

query {
 viewer {
    login
    starredRepositories(last :5){
      nodes {
        name
      }
    }
  }
}

GraphQLはどんな場面に向いているか

GraphQLを使用するとクライアントが必要なフィールドだけを必要な形で取得できるようになる。
その結果、クライアントの要求に対するサーバー側の対応が減る。
この特徴はどんなときに活きるのか?

それは、クライアントの仕様変更がAPIサーバーへ波及するのを防ぎたい場合のとき
クライアントが欲しい形のデータを1回で取得できるGraphQLが有効であるといえる。

GraphQLのスキーマに従いクエリを書くことによりクライアントはGraphQL APIから必要なデータだけを引き出すことができる。
この点で、GraphQLにはスキーマが必須で、スキーマ駆動開発を実現しやすい。

「特定少数の開発者」(SSKDs)向けWeb APIとしてGraphQLを利用するケース

今回の例では、APIの実装に「Apollo Server」を使用する。

Apollo Server

Node.jsでGraphQLサーバーを実装できる、Apolloが提供するツ―ル。
また、ダミーデータを返すスタブサーバーのようにスキーマ駆動でGraphQL APIを開発しやすくなる機能も使用できるようになる。

GraphQL APIの開発フロー

  • 1.GraphQLのスキーマを書く
  • 2.チーム内でスキーマについてレビューする
  • 3.チームに従いAPIサーバーとクライアントを並行作業で実装する
  • 4.APIサーバとクライアントを結合する

GraphQLのAPIサーバーはスキーマで定義された型が持つフィールドの値を「リゾルバ」(resolver)という一種のハンドラを実装することで取得できるようにする。
今回はApollo Serverの機能によりリゾルバが返す値をダミーデータに置き換え、スタブサーバーを使用できるようにする。

スタブサーバーを使用した後はリゾルバにアプリケーション固有のロジックを実装しながらサーバー開発を進めることとなる。
クライアントはスタブサーバーを使用し、サーバーがリゾルバを実装できれば無事サーバーとクライアントを結合できるようになる

匿名掲示板をApollo Serverで作るよ。

まず、Appollo Serverをインストールする。

$npm install --save apollo-server graphql

次にindex.jsというファイルに簡単なGraphQLサーバーを書いてみる。
文字列型のcontentというフィールドだけをスキーマに含むGraphQLサーバーは次のようになる。

  • index.js
const {AplloServer, gql}
  = require(`apollo-server-express`);
const typeDefs = gql`
  type Query {
    const: String
  }
  `;
  const resolver = {
    Query: {
      content: () => 'こんにちは'
    }
  };  

ここではresolversというオブジェクトが先ほど証明したリゾルバの役割を果たしている。

フィールドの値の取得をクライアントにクエリとして指示する

APIサーバーはフィールドに登録したリゾルバを実行して値を取得

※GraphQLエンドポイントは慣習的に単一の/graphqlとすることが多い

このサーバーを立ち上げるためには次のコマンドを実行する

$ node index.js
Server started at http://localhost:4000/graphql

GraphQLを使用し次のクエリを送信してみる

  • http://localhost:4000/graphqlへ送信するクエリ
query{
  content
}
  • スタブサーバーのレスポンス
{
  "data":{
    "content": "こんにちは"
  }
}

実際はある程度込み入ったロジックをAPIサーバーとしてある程度込み入ったロジックを実装していく必要がある。
その際、スキーマからスタブサーバーを生成できるとサーバーと並行してクライアントを開発できる

Apollo Serverでスタブサーバーを作成する

Apollo ServerはMockingというスタブサーバー作成の機能を含んでいる。
これは、GraphQL ToolsというApollo Serverの基盤ライブラリで実現している。
String!

Apollo Serverでスタブサーバーを作成していく

まず、スキーマ駆動開発の基点として、次のGraphQLのスキーマを記述済みとする。
開発フロー全体を通しこのスキーマを使用する。

Apollo Serverからスキーマを扱えるようにモジュールとしてexportしておく。

  • type_defs.js
const {gql} = require{'apollo-server-express'};

exports.type.Defs = gql'
type Query {
  posts: [POST]! 
  post(id: ID!): Post
}

type Post {
id: ID!
title: String!
content: String!
posted At: String!
comment: [Comment]!
}

type Comment {
  id: ID!
  content: String!
  commentedAt: String!
  post: Post!
}

input PostInput {
  title: String,
  content: String
}

type Mutation {
createPost(input: PostInput): Post
}
//そのほかのミューテーション(作成、更新、削除)は省略

次にこのスキーマを使用しスタブサーバを作成し、起動させる。

  • index.js
const {ApolloServer} = require('apollo-server-express');
const {typeDefs} = require('./type_defs');

// mocks: trueでMocking機能を有効にする
const server = new ApolloServer({typeDefs, mocks: true});

const express = require('express');
const app = express();
server.applyMiddleware({app, path: '/graphql'});

app.listen({port: 4000}, () =>
  console.log(`Server started at http://localhost:4000$
  {server.graphqlPath}`)
  );

このようにGraphQLのスキーマtypeDefsとオプションmocks: trueをApolloServerのコンストラクタに渡すことでスタブサーバーを立ち上げることができる。

同じように、サーバーとして立ち上げてGraphQLでクエリを送信してみよう。
ある投稿とそのコメントを取得するクエリを http://localhost:4000/graphql へ送信すると次のようになる。

  • http://localhost:4000/graphqlへ送信するクエリ
query {
  posts {
   id
   title
   content
   comments {
    id
    content
    }
  }
}
  • スタブサーバーのレスポンス(idの値は異なる)
{
    "data": {
      "posts": [
        {
          "id" "*********-*********-*********"//idの値は適当
          "title": "Hellow World",
          "content": "Hellow World",
          "comments": [
          {
            "id": "*********-*********-*********",
            "content": "Hello World"
          }
        ]
      },
      {
          "id" "*********-*********-*********"//idの値は適当
          "title": "Hellow World",
          "content": "Hellow World",
          "comments": [
        {
            "id": "*********-*********-*********",
            "content": "Hello World"      
        }
        {
            "id": "*********-*********-*********",
            "content": "Hello World"      
        }
      ]
    }
  ]
  }
}

これを見ると、スキーマに対応したレスポンシブが返ってきていることがわかる。
ただ、ここではフィールドの値が型に応じたデフォルト値になっている。

String型 → ”Hellow World”
ID型 → UUID(Universally Unique Identifer) 

となっている。これでも使えないことがないが、OpenAPI利用時にスキーマから生成したスタブサーバーのように実例にある程度近い値を返してもらったほうがUIの確認がやりやすそう。

GraphQL ToolsのMockingという機能でスタブサーバーが返す値を設定できる。
それには次のようなコードmock.jsを書く。

  • mock.js
export.mocks = {
  ID: () => 1,
  Post: () => ({
    title: 'こんにちは'
    content: '今日は良い天気ですね',
    posted_at: '2018-12-01T00.00:00Z',
    }),
    Comment: () => ({
      content: 'そうですね',
      posted_at: '2018-12-01T00.00:00Z',
    }),
  };

オブジェクトのキー名はGraphQLにおける型名と同じものとする。
フィールドにダミーデータを返す値を記述する。

※ここでは実例に近い値を書くようにするとよい

また、mock.jsをスタブサーバーで利用できるようindex.jsを次のように置き換えサーバーを立ち上げる。

  • index.js
const {ApolloServer,ggl}
  = require('appolo-server-express');

  const {typeDefs} = require('./type_defs.js');
  const resolvers = {};
  const {mocks} = require('./mocks.js');

  const server
    = new ApolloServer({typeDefs, resolver, mocks});
    const express = require('expres');
    const app = express();
    server.app = express();
    server.applyMiddleware({app, path: '/graphql'});
      cosole.log(
        'Server started at http://localhost:4000${server.graphPath}'
      )
    );

これまでと同様に、GraphiQLからクエリを送ってみる。
すると、スタブサーバーからは次のようなレスポンスが返ってくる。

  • スタブサーバーのレスポンス
{
"data": {
    "post": [
      {
        "id": "1",
        "title": "こんにちは",
        "conmtent": "今日は良い天気ですね",      
        "comments": [
          {
            "id": "1",
            "content" "そうですね"
          }
          {
            "id": "1",
            "content" "そうですね"
          }
        ]
      },
      {  
        "id": "1",
        "title": "こんにちは",
        "conmtent": "今日は良い天気ですね",      
        "comments": [
          {
           "id": "1",
            "content" "そうですね"
          }
          {
            "id": "1",
            "content" "そうですね"
          }
        ]
      }
    ] 
  }
}

このように、mock.jsで記述した実例により近い値をフィールドの値として持つレスポンスが返ってくるようになる

スキーマ駆動開発の手順として、

  • スタブサーバーを作りクライアント開発に利用してもらう
  • その間にスキーマのとおりにリゾルバを実装してもらい、実際のGraphQLサーバーを作る

inex.js

const {ApolloServer,ggl}
  = require('apollo-server-express');
const {typeDefs} = require('./type_defs.js');

let post = [
{id:1, title: 'こんちは' content: '今日は良い天気ですね', createdAt: '2018-12-01T00:00:00'}
{id: 2, title: 'こんばんは', content: '今日は曇りですね', createdAt: '2018-12-01T00:00:00'}
];
let comments = [
{id:1, comments: 'そうですね', postID: 1, createdAt: createdAt: '2018-12-01T00:00:00'},
{id:2, comments: 'ちょっと寒いですね', postID: 2, createdAt: createdAt: '2018-12-01T00:00:00'},
{id:3, comments: '雨が降りそうですね', postID: 2, createdAt: createdAt: '2018-12-02T00:00:00'},
];
const resolver = {
  Query: {
    posts(){
      return posts;
    }
},
Post: {
  comments(post) {
    return comments.filter(
      (comment) => {return comment.postId === post.id }
    );
    },
  ) 
    postedAt(post) {
    return post.createdAt;
  }
  Comments: {
    commentedAt(comment){
    return comment.createdAt;
  }
}
};
const server = new AppoloServer({typeDefs, ressolver});
const express = require('express');
const app = express();
server.applyMiddleware({app, path: '/graphql'});
app.listen({port:4000}, () =>
  console.log(
    'Server started at http://localhost:4000${server.graphqlPath}'
  )
);

ここでAPIサーバーが返す値の型がスキーマと異なる場合はどうなる?
先ほどのコードでpostsの1個目の要素のtitleがnullになるように変更してみる。

すると、次のようなエラーになる

  • postsのtitleがnullの場合
{
  "errors":[
    {
    "message": #Connect return null for non-nullable field Post.title. ",
      "locations": [
      {
        "line": 4,
        "colum": 5
      }
     ],
     "path": [
      "posts",
      0,
      "title" 
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
          ]
         }
        }
      }
    ],
    "data" {
      "posts": [
         null
      {
        "id": "2",
        "title": "こんばんは"
      }
      ]
    }  
  }

これは、GraphiQLの実行環境がスキーマと比較しレスポンスが妥当か検証してくれることによる。
APIサーバーのテストでクエリをエンドツーエンドで実行しておくとこのエラーをより発見しやすくなる

GraphQLはスキーを仕様として持っており、自然にスキーマから書き始められる点がスキーマ駆動開発と親和性が高い理由の一つ

これまで記事をまとめてみて

スキーマ駆動 WebAPI開発「OprnAPIによるスキーマ駆動開発」「GraphQLによるスキーマ駆動開発」

OprnAPIによるスキーマ駆動開発 匿名掲示板用APIをサンプルに

REST APIのスキーマ駆動開発について具体的に見ていく。
こちらの記事で紹介した、匿名掲示板用APIをREST APIとして開発し、クライアントから利用するという状況を想定。

スキーマ駆動開発の開発フローは下記になる。

  • 1.実装する機能で必要になるAPIエンドポイントのスキーマを設計する
  • 2.チーム内でスキーマについてレビューする
  • 3.スキーマに従ってAPIサーバーとクライアントを平行作業で実装する
  • 4.APIサーバーとクライアントを結合する

クライアントとAPIサーバーの開発方法

匿名掲示板APIをREST開発 → クライアントから利用する という状況を想定。

クライアント

クライアントの開発ではスキーマ(本記事でのOpenAPI ドキュメント)とOpenAPI Generatorを利用する。
OpenAPI Generatorを使用し、スキーマからクライアントのコードとスタブサーバーを生成する。

本物のAPIサーバーの実装が完了するまではスタブサーバーを利用して開発を進める。
APIエンドポイントへのアクセスには生成したAPIクライアントを利用することで実装工数を減らすことができる。

APIサーバー

APIサーバーの開発においてはスキーマと実装の乖離を防ぐことが重要
スキーマに定義したリソースのSchemaオブジェクトを利用して、スキーマに準拠するAPIエンドポイントを実装する。

そうすることでスキーマと実際のレスポンスの間に乖離がないかを検証することができる。

クライアント側の開発を実装(駆動)

こちらの記事では、OpenAPI Generotorを使用している。
上記記事にてSwagger CodegenではなくOpenAP Generotorを用いているのは執筆時点でSwagger CodegenはOpenAPI Specification バージョン3に対応していなかったから。

※Swagger Codegenは2019.04.25にOpenAPI Specification バージョン3に対応。

OpenAPI Generotorって?

Web APIのスキーマからAPIクライアントやスタブサーバーを生成できるツール。
OpenAPI GenerotorはAPIクライアントとスタブサーバーそれぞれにおいて各言語のHTTPクライアントライブラリやWebアプリケーションフレームワークの種類を指定することができる。

OpenAPI GenerotorはSwagger Codegenよりフォークしたプロジェクト。

まずはスキーマからAPIクライアントを生成

このAPIクライアントは次のような機能をもつ。

  • APIの各エンドポイントに対応するメソッドをもつ
  • APIリクエスト用のオブジェクトからスキーマの中で定義したJSONのキーの型を考慮しリクエストボディへ変換する
  • APIがレスポンスとして返すJSONを、スキーマ中で定義したJSONのキーの型を考慮してオブジェクトへ変換する

OpenAPI Generatorはスキーマ上で定義した属性の型に基づいてクライアントライブラリを生成することができる。
具体的にはAPIクライアントの利用者がオブジェクトをもとにリクエストを送ったりレスポンスを受け取ったりするときにAPIクライアントがJSONとオブジェクトの間のマッピングを担ってくれる

これにより仮にスキーマの定義に変更があっても、APIクライアントをふたたび生成すれば新しい定義のスキーマに対応できることがわかる。

実際にAPIクライアントを生成する その前に

ここでは
– OpenAPI Generator3.3.4
– 公式のDockerイメージ
を利用する。
あらかじめ、Docker Storeから使用するプラットフォームに対応したDocker Community Editionをインストールしておく。

いよいよ匿名掲示板API用・JavaScriptクライアントを生成する

下記コマンドを実行。

$ docker run --rm -v ${PWD}:/local \
openapitools/openapi-generator-cli generate \
-g javascript -DprojectName='anon-bbs-api' \
-i /local/openapi.yaml -o /local/javascript_client

生成したコードは下記のようになっている。

  • OpenAPI Generatorで生成したJavaScriptクライアント(一部)
javascript_client
|-READE.md
|-docs
|  |-(省略)
|-package.json
|-src
|  |-ApiClients.js
|  |-api
|  |  DefaultApi.js   
|  |
|  |-index.js
|  |  model
|  |  |-Comment.js
|     |-CommentParameters.js
|     |-CommentProperties.js
|     |-CommentRequest.js
|     |-Comments.js
|     |-ErrorProperties.js
|     |-Errors.js
|     |-Post.js
|     |-PostParameters.js
|     |-PostProperties.js
|     |-PostRequest.js
|       Posts.js
|_test
    -(省略)

APIクライアントの実体はsrc配下に配置される。
このクライアントライブラリをローカルで使えるようにする為、下記コードを記述。

$ cd javascript_client
$ npm link
$ cd ~/myapp ←自分のアプリケーションディレクトリへ移動する
$ npm link anon-bbs-api

APIクライアントは次のように使用。
~/myapp/index.js

var AnonBbsApi = require('annon-bbs-api');
var api = new AnonBbsApi.DefaultApi()
var postRequest = AnonBpsApi.PostRequest.constructFromObject(//ApiClientが持つスキーマで定義された型へ変換するためのメソッドを利用している
{
  post: {
  title: "こんには",
  content: "良い天気ですね"
  }
});
api.createPost( //APIエンドポイントPOST/postsを呼び出すためにメソッドcreatePostを使用している
  postRequest, //Postクラスのオブジェクトを渡している。OpenAPIにおけるcomponentに対応するModelクラスが生成されている為これを利用しリクエストボディとして使うPostクラスのオブジェクトを生成することができる
  function(error,data,response){
  if(error){
    console.error(error);
   } else { 
   console.log('response':' + response.text);
   }
}
);

「http://localhost:3000」 にてAPIサーバーを立ち上げ

クライアントのサンプルコードを実行

APIサーバーのエンドポイントPOST/postへリクエスト。

クライアントは投稿作成用のエンドポイントへ正常なパラメーターを送信している為、APIサーバー側は正常なレスポンスを返す

クライアントは成功時のメッセージを標準出力へ表示する。

$ node index.js
response: {"post":{"id":3,"title":"こんにちは","content":"良い天気ですね","posted_at":"2018-11-30T01:05:04Z"}}

スキーマからコードを生成することにより、APIサーバーとやりとりするデータの型とスキーマが定義している型とが乖離しないような仕組みになっている

次にスタブサーバーの生成

スタブサーバーの生成はAPIクライアントの生成とほぼ同じコマンドで行うことができる。
本記事ではJavaのSpring Boot1で稼働するスタブサーバーを生成する。

OpenAPI Generatorで生成したSpring Bootで動くスタブサーバーはスキーマの中のcomponetsに定義したSchemaオブジェクトのexampleフィールドが持つ値をレスポンスのJSONのキーが持つ値として利用することができる。

つまり、スキーマにダミーデータを書いておくことができるということ。

スタブサーバーを生成するコマンド

$ docker run -v {PWD}:/local \ 
openapitools/openapi-generator-cli generate \
-i /local/openapi.yaml -g spring  -o /local/spring_stub \
--additional-properties returnSucessCode=true

スタブサーバーを立ち上げ利用する

カレントディレクトリに作られたspring_subディレクトリへ移動

スタブサーバーを立ち上げるためのコマンドを実行

Dockerイメージのmavenとjavaを利用しSpring Bootアプリケーションを起ち上げる

$cd ./spring_stub
$docker run -- v ${PWD}:/usr/src/mymaven \ -w /usr/src/mymaven maven mvn package
$docker run --rm -p 3000:3000 \ -v ${PWD}:/usr/src/myapp -w /usr/src/myapp \java java -jar target/openapi-spring-1.0.0.jar

http://localhost:3000にスタブサーバーが起動する

スタブサーバーへHTTPリクエストを送信してみる

下記のようにダミーデータが返ってくる

$curl -s http://localhost:3000/posts |jq .
{
  "posts":[
  {
    "id": 1,
    "title": "こんにちは",
    "posted_at": "2000-01-23T04:56:07.000+00:00",
    "content":"今日は良い天気ですね"
  },
  {
    "id": 1,
    "title": "こんにちは",
    "posted_at": "2000-01-23T04:56:07.000+00:00",
    "content":"今日は良い天気ですね"
  }
]
}

このようにスキーマにAPIエンドポイントとそのレスポンスを定義しておくと簡単にスタブサーバーを立ち上げることができる
実際にのAPIサーバーが実装完了するまではこのスタブサーバーを使ってクライアントの開発を進めることができる。

スキーマを使ったAPIサーバーの開発

スキーマ駆動開発においては、スキーマはWeb APIの仕様についての一時情報となる。
APIサーバーを実装する際はそのレスポンスが含むデータ構造がスキーマ上のレスポンス定義と一致するよう気を付ける必要がある。

  • Postの持つフィールドを表すSchemaオブジェクト
type: object
properties:
  id:
    type: integer
    example: 1
  title: 
    type:string
    example: 良い天気ですね
  content:
    type: string
    example: 良い天気ですね
  posted_at:  
    type: string
    format: data-time
    example: '2018-12-01T00:00:00Z'
  required:
    -id
    -title
    -content
    -posted_at

上記を行う際、APIサーバーがPostについて次のようなレスポンスを返してしまうとキー名が違いクライアントがうまく値を取得できなかったり、取得した値の型が違ったりといった問題が発生する

  • Postを表すAPIサーバーのレスポンスJSON・間違った例
{
"id": "1"//intgerでなければいけないのにstringになってる
"title": "こんにちは"
"content": "良い天気ですね"
"submitted_at": "2018-12-01T00:00:00Z"//キー名が誤ってる
}

上記の問題を防ぐためにもスキーマを活用するとよい。
APIサーバーの自動テストでチェックする方法がおススメ。

CI(Continnous Integration、継続的インテグレーション)でテストを実行しておくことで、もし定義と実際のレスポンスの間に解離が発生したとしてもすぐに発見することができる。

実際にテストを行う方法について見てみる。
環境として、下記を用いる。

  • APIサーバー実装時のWebアプリケーションフレームワーク:Ruby on Rails 5.2.1.1
  • テスティングフレームワーク:23.8.0

まず、掲示板APIのスキーマで定義したエンドポイントPOST /post を次のように実装しているとする。

  • config/routes.rb
Rails.application.routes.draw do
 resource :posts, only: [:create]
end
  • app/contorollers/application_controller.rb
class ApplicationController < ActionController::API
end
  • app/contorollers/posts_controller.rb
class PostsController < ActionController::API
  # id、title、content、created_atをカラムとして持つテーブルpostsのレコードを保存する
  def create 
    @post = Post.new(post_params)

  if @post.save
    render status: :created 
  else
    # エラーのレンダリング
  end
end

private

  def post_params
    params.require(:post).permit(:title, :content)
  end
end
  • app/views/posts/create.json.jbuilder
json.post do
  json.(@post, id,title , :content)
  json.posted_at @post.create_at.iso8601
end

上記エンドポイントに対し、RSpecのrequest specは次のようになる。

  • spec/requests/posts_spec.rb
require 'rails_helper'

Rspec.describe 'Posts', type: :request do
  describe 'POST' /posts' do
    context 'when params are valid' do
    let(:code) { 201 }

    it 'creates a post and returns 201' do
      post '/posts'
        headers: {
          ACCEPT: 'application'/json'
          CONTENT_TYPE: 'application/json'
          }, 
        params: {
          post: {
            title: 'テストのタイトル',
            content: 'テストの本文'
            }
          }.to_json
       expect(response.status).to eq code 
       # !ここでAPIサーバーのレスポンス構造がスキーマと一致するかテストする!
       end
    end
  end
end

SchemaオブジェクトとJSON Schemaライブラリを用いてJSONのバリエーションを行う

まず、OpenAPIのSchemaオブジェクトを用いたJSONのバリデーション方法について解説。

スキーマからテストするエンドポイントに対応するOperationオブジェクトが持つResponseオブジェクトを取得する。
そして、対応するステータスコードのResponseオブジェクトが持つcontentフィールドの値であるSchemaオブジェクトを使用し、レスポンスに対するバリエーションを行う。

今回はOpenAPIのバージョン3に対応しているgemであるoas_parser0.14.0を用いてスキーマをパースする。

# スキーマをパースしてRubyのオブジェクトにする
spec = OasParser::Definition.resolve('openapi.yaml')
# POST /postの成功時(201)レスポンスを表すSchemaオブジェクトを取得
schema_data = spec.path_by_path('posts')
                .endpoint_by_method('post')
                .response_by_code('201')
                .content
                .dig('application/json', 'schema')

上記schema_dataはハッシュになっている

次に、3というgemとスキーマから得たSchemaオブジェクトを使用し実際のレスポンスがSchemオブジェクトに準拠しているのかバリデーションエラーを実行する

schema = JsonSchema.parse!(schema_data)
//このJSON Schemaインスタンスを利用し次のJsonSchema#validateメソッドで次のようにレスポンスのバリデーションを実行することができる

schema.validate({
  'post' => {
  'id'  => '1',
  'title'  => 'こんにちは', 
  'content' =>  '今日はくもりですね',
  'post_at' =>  '2018-12-01T15:00:00Z'
  }
})
=> [false,[#<JsonSchema::ValidationError: #/post/id: failed schema #/properties/post/properties/id: For 'properties/id', "1" is not an integer.>]]

//上記例はpostのキーの一つであるidの値の型がスキーマ上は整数である一方、JSONでは文字列型になっている為バリデーションエラーとなっRている

これまで準備した仕組みを用いると、RSpecのrequest specsを用いてAPIサーバーの実際のレスポンスを取得しそのレスポンスがスキーマ上のレスポンス定義と一致するかをテストできるようになる。

beforeブロックで実行しているSchemaオブジェクトの探索を各テストで実行できるようにヘルパメソッドにしておき、request specs実行前のフックとして実行できるようにしておく。

  • spec/support/request_specs_helper.rb
module RequestSpecsHelper
  def expect_to_conforms_schema(response)
    expect {
    @schema.validate!(JSON.parse(response.body))
    }.not_to raise_error
  end
end
  • spec/rails_helper.rb
#(省略)
RSpec.configure do |config|
#(省略)
  config.include RequestSpecsHelper
  config.before :example,type: :request do
    spec = OasParser::Difinition.resolve(
      Rails.root.join('public','openapi.yaml')
    )
   schema_data = spec.path_by_path(schema_path)
   .endpoint_by_method(schema_method)
   .response_by_code(code.to_s)
   .content
  #レスポンス定義が存在しないケース(204など)の対応として&.を利用
  &.dig('application/json', 'schema')
  @schema = JsonSchema.parse!(schema_data) if schema_data
  end
  end
  • spec/requests/posts_specs.rb
RSpec.describe 'Posts', type: :request do
 describe 'POST' /posts' do
  let(:schema_method) { 'post' }
  let(:schema_path) { '/post' }

  context 'when params are vaild' do
    let(:code) {201}

    it 'create a post' do
      post '/posts',
            headers: {
              ACCEPT: application/json'
              CONTENT_TYPE: 'application/json'
              },
             params: {
              post:
                title: 'テストのタイトル',
                content 'テストの本文'
             } 
            }.to_json
  expect(response.status).to eq code
  expect_to_conform_schema response
  end
  end
  end
end

ここまででスキーマを用いてスキーマを使用し、APIサーバーからのレスポンスがスキーマで期待する構造を持っているかを検証できるようになった。
以上の仕組みをいれておくと、エンドポイントのテスト追加時にスキーマと実装が乖離していないかどうかのチェックするテストを書けるようになる

実際にはこのような仕組みをもとに動くライブラリを用いてスキーマに基づいたAPIサーバーへの入出力のバリデーションを実行することが多い。

OpenAPIのSchemaオブジェクトを用いたバリデーションをより厳密にできる便利なフィールドとして下記のようなものがある。

  • additionalProperties:キーの存在可否をバリデーションする
  • required:必須プロパティをバリデーションする
  • nullable:値としてnullを許可するプロパティのバリデーション

Swagger UIを用いるとスキーマをもとにブラウザで参照できるWeb APIのリファレンスを生成し、そのリファレンス上でAPIサーバーへリクエストを送ることができる。

流れは下記。

  • Swagger UIを起動する
  • Swagger UIからAPIサーバーへアクセスするための設定を行う
  • Swagger UIでWeb APIの仕様をブラウザする

Swagger UIを用いるとインタラクティブなAPIリファレンスを手に入れることができ、スキーマの定義と一致した実装を持つAPIサーバーと組み合わせることで開発者がすぐAPIサーバーの概要を把握できるようになる。

OprnAPIによるスキーマ駆動開発 匿名掲示板用APIをサンプルに!のまとめ

チームで作り上げたスキーマを用いてAPIサーバーとそのクライアントの開発を駆動する方法について学んだ。
スキーマ駆動で開発を進めることでREST APIをサーバーとクライアントのインターフェイスとするアプリケーションの開発をこれまで以上に効率的に進めることができるだろう。

GraphQLによるスキーマ駆動開発~API仕様とクエリ言語を組み合わせて柔軟にデータを取得する~

GraphQLとは

Web APIのためのクエリ言語とその実行環境で構成されている。
GraphQLを用いたWeb APIはREST APIとは異なった形式のインターフェイスを持ち、また、仕様にスキーマという概念を含んでいる。

GraphQLを用いたWeb APIの実例

有名なのはGitHubのGraphQL API。
このAPIを利用してみる。

また、ここではGraphiQLというツールを使用する。
GraphiQLはクエリの編集が煩雑になりがちという問題をブラウザ上のGUI経由でGraphQL APIへクエリを送信できるように解決している

GraphiQLはGraphQLのAPIドキュメントの表示やクエリ補完にも対応している。

GraphQL APIへクエリを送信する流れ

ここで使用するのはElectronによるデスクトップアプリ版のGraphiQL.app0.7.2を用いる。

  • 1.あらかじめ自身のGitHubアカウントでpersonal acceess tokenを作成しておく。
  • 2.GraphiQLを起動する
  • 3.右上の「Edit HTTP Headers」をクリックする
  • 4.「+ Add Header」ボタンをクリックし、「Header name」をAuthorization、「Header value」をBearer<GitHubから取得したpersonal access token>の形式で入力し「Save」をクリックし保存する。
  • 5.「GraphQL Endpoint」に https://api.github.com/graphqlを入力し、GraphiQLの左側のエリアにクエリを入力する
  • 6.右向きの三角形のボタンをクリック

  • GitHubのGraphQL APIへ送信するクエリ

query {
 viewer {
    login
    starredRepositories(last :5){
      nodes {
        name
      }
    }
  }
}

GraphQLはどんな場面に向いているか

GraphQLを使用するとクライアントが必要なフィールドだけを必要な形で取得できるようになる。
その結果、クライアントの要求に対するサーバー側の対応が減る。
この特徴はどんなときに活きるのか?

それは、クライアントの仕様変更がAPIサーバーへ波及するのを防ぎたい場合のとき
クライアントが欲しい形のデータを1回で取得できるGraphQLが有効であるといえる。

GraphQLのスキーマに従いクエリを書くことによりクライアントはGraphQL APIから必要なデータだけを引き出すことができる。
この点で、GraphQLにはスキーマが必須で、スキーマ駆動開発を実現しやすい。

「特定少数の開発者」(SSKDs)向けWeb APIとしてGraphQLを利用するケース

今回の例では、APIの実装に「Apollo Server」を使用する。

Apollo Server

Node.jsでGraphQLサーバーを実装できる、Apolloが提供するツ―ル。
また、ダミーデータを返すスタブサーバーのようにスキーマ駆動でGraphQL APIを開発しやすくなる機能も使用できるようになる。

GraphQL APIの開発フロー

  • 1.GraphQLのスキーマを書く
  • 2.チーム内でスキーマについてレビューする
  • 3.チームに従いAPIサーバーとクライアントを並行作業で実装する
  • 4.APIサーバとクライアントを結合する

GraphQLのAPIサーバーはスキーマで定義された型が持つフィールドの値を「リゾルバ」(resolver)という一種のハンドラを実装することで取得できるようにする。
今回はApollo Serverの機能によりリゾルバが返す値をダミーデータに置き換え、スタブサーバーを使用できるようにする。

スタブサーバーを使用した後はリゾルバにアプリケーション固有のロジックを実装しながらサーバー開発を進めることとなる。
クライアントはスタブサーバーを使用し、サーバーがリゾルバを実装できれば無事サーバーとクライアントを結合できるようになる

匿名掲示板をApollo Serverで作るよ。

まず、Appollo Serverをインストールする。

$npm install --save apollo-server graphql

次にindex.jsというファイルに簡単なGraphQLサーバーを書いてみる。
文字列型のcontentというフィールドだけをスキーマに含むGraphQLサーバーは次のようになる。

  • index.js
const {AplloServer, gql}
  = require(`apollo-server-express`);
const typeDefs = gql`
  type Query {
    const: String
  }
  `;
  const resolver = {
    Query: {
      content: () => 'こんにちは'
    }
  };  

ここではresolversというオブジェクトが先ほど証明したリゾルバの役割を果たしている。

フィールドの値の取得をクライアントにクエリとして指示する

APIサーバーはフィールドに登録したリゾルバを実行して値を取得

※GraphQLエンドポイントは慣習的に単一の/graphqlとすることが多い

このサーバーを立ち上げるためには次のコマンドを実行する

$ node index.js
Server started at http://localhost:4000/graphql

GraphQLを使用し次のクエリを送信してみる

  • http://localhost:4000/graphqlへ送信するクエリ
query{
  content
}
  • スタブサーバーのレスポンス
{
  "data":{
    "content": "こんにちは"
  }
}

実際はある程度込み入ったロジックをAPIサーバーとしてある程度込み入ったロジックを実装していく必要がある。
その際、スキーマからスタブサーバーを生成できるとサーバーと並行してクライアントを開発できる

Apollo Serverでスタブサーバーを作成する

Apollo ServerはMockingというスタブサーバー作成の機能を含んでいる。
これは、GraphQL ToolsというApollo Serverの基盤ライブラリで実現している。
String!

Apollo Serverでスタブサーバーを作成していく

まず、スキーマ駆動開発の基点として、次のGraphQLのスキーマを記述済みとする。
開発フロー全体を通しこのスキーマを使用する。

Apollo Serverからスキーマを扱えるようにモジュールとしてexportしておく。

  • type_defs.js
const {gql} = require{'apollo-server-express'};

exports.type.Defs = gql'
type Query {
  posts: [POST]! 
  post(id: ID!): Post
}

type Post {
id: ID!
title: String!
content: String!
posted At: String!
comment: [Comment]!
}

type Comment {
  id: ID!
  content: String!
  commentedAt: String!
  post: Post!
}

input PostInput {
  title: String,
  content: String
}

type Mutation {
createPost(input: PostInput): Post
}
//そのほかのミューテーション(作成、更新、削除)は省略

次にこのスキーマを使用しスタブサーバを作成し、起動させる。

  • index.js
const {ApolloServer} = require('apollo-server-express');
const {typeDefs} = require('./type_defs');

// mocks: trueでMocking機能を有効にする
const server = new ApolloServer({typeDefs, mocks: true});

const express = require('express');
const app = express();
server.applyMiddleware({app, path: '/graphql'});

app.listen({port: 4000}, () =>
  console.log(`Server started at http://localhost:4000$
  {server.graphqlPath}`)
  );

このようにGraphQLのスキーマtypeDefsとオプションmocks: trueをApolloServerのコンストラクタに渡すことでスタブサーバーを立ち上げることができる。

同じように、サーバーとして立ち上げてGraphQLでクエリを送信してみよう。
ある投稿とそのコメントを取得するクエリを http://localhost:4000/graphql へ送信すると次のようになる。

  • http://localhost:4000/graphqlへ送信するクエリ
query {
  posts {
   id
   title
   content
   comments {
    id
    content
    }
  }
}
  • スタブサーバーのレスポンス(idの値は異なる)
{
    "data": {
      "posts": [
        {
          "id" "*********-*********-*********"//idの値は適当
          "title": "Hellow World",
          "content": "Hellow World",
          "comments": [
          {
            "id": "*********-*********-*********",
            "content": "Hello World"
          }
        ]
      },
      {
          "id" "*********-*********-*********"//idの値は適当
          "title": "Hellow World",
          "content": "Hellow World",
          "comments": [
        {
            "id": "*********-*********-*********",
            "content": "Hello World"      
        }
        {
            "id": "*********-*********-*********",
            "content": "Hello World"      
        }
      ]
    }
  ]
  }
}

これを見ると、スキーマに対応したレスポンシブが返ってきていることがわかる。
ただ、ここではフィールドの値が型に応じたデフォルト値になっている。

String型 → ”Hellow World”
ID型 → UUID(Universally Unique Identifer) 

となっている。これでも使えないことがないが、OpenAPI利用時にスキーマから生成したスタブサーバーのように実例にある程度近い値を返してもらったほうがUIの確認がやりやすそう。

GraphQL ToolsのMockingという機能でスタブサーバーが返す値を設定できる。
それには次のようなコードmock.jsを書く。

  • mock.js
export.mocks = {
  ID: () => 1,
  Post: () => ({
    title: 'こんにちは'
    content: '今日は良い天気ですね',
    posted_at: '2018-12-01T00.00:00Z',
    }),
    Comment: () => ({
      content: 'そうですね',
      posted_at: '2018-12-01T00.00:00Z',
    }),
  };

オブジェクトのキー名はGraphQLにおける型名と同じものとする。
フィールドにダミーデータを返す値を記述する。

※ここでは実例に近い値を書くようにするとよい

また、mock.jsをスタブサーバーで利用できるようindex.jsを次のように置き換えサーバーを立ち上げる。

  • index.js
const {ApolloServer,ggl}
  = require('appolo-server-express');

  const {typeDefs} = require('./type_defs.js');
  const resolvers = {};
  const {mocks} = require('./mocks.js');

  const server
    = new ApolloServer({typeDefs, resolver, mocks});
    const express = require('expres');
    const app = express();
    server.app = express();
    server.applyMiddleware({app, path: '/graphql'});
      cosole.log(
        'Server started at http://localhost:4000${server.graphPath}'
      )
    );

これまでと同様に、GraphiQLからクエリを送ってみる。
すると、スタブサーバーからは次のようなレスポンスが返ってくる。

  • スタブサーバーのレスポンス
{
"data": {
    "post": [
      {
        "id": "1",
        "title": "こんにちは",
        "conmtent": "今日は良い天気ですね",      
        "comments": [
          {
            "id": "1",
            "content" "そうですね"
          }
          {
            "id": "1",
            "content" "そうですね"
          }
        ]
      },
      {  
        "id": "1",
        "title": "こんにちは",
        "conmtent": "今日は良い天気ですね",      
        "comments": [
          {
           "id": "1",
            "content" "そうですね"
          }
          {
            "id": "1",
            "content" "そうですね"
          }
        ]
      }
    ] 
  }
}

このように、mock.jsで記述した実例により近い値をフィールドの値として持つレスポンスが返ってくるようになる

スキーマ駆動開発の手順として、

  • スタブサーバーを作りクライアント開発に利用してもらう
  • その間にスキーマのとおりにリゾルバを実装してもらい、実際のGraphQLサーバーを作る

inex.js

const {ApolloServer,ggl}
  = require('apollo-server-express');
const {typeDefs} = require('./type_defs.js');

let post = [
{id:1, title: 'こんちは' content: '今日は良い天気ですね', createdAt: '2018-12-01T00:00:00'}
{id: 2, title: 'こんばんは', content: '今日は曇りですね', createdAt: '2018-12-01T00:00:00'}
];
let comments = [
{id:1, comments: 'そうですね', postID: 1, createdAt: createdAt: '2018-12-01T00:00:00'},
{id:2, comments: 'ちょっと寒いですね', postID: 2, createdAt: createdAt: '2018-12-01T00:00:00'},
{id:3, comments: '雨が降りそうですね', postID: 2, createdAt: createdAt: '2018-12-02T00:00:00'},
];
const resolver = {
  Query: {
    posts(){
      return posts;
    }
},
Post: {
  comments(post) {
    return comments.filter(
      (comment) => {return comment.postId === post.id }
    );
    },
  ) 
    postedAt(post) {
    return post.createdAt;
  }
  Comments: {
    commentedAt(comment){
    return comment.createdAt;
  }
}
};
const server = new AppoloServer({typeDefs, ressolver});
const express = require('express');
const app = express();
server.applyMiddleware({app, path: '/graphql'});
app.listen({port:4000}, () =>
  console.log(
    'Server started at http://localhost:4000${server.graphqlPath}'
  )
);

ここでAPIサーバーが返す値の型がスキーマと異なる場合はどうなる?
先ほどのコードでpostsの1個目の要素のtitleがnullになるように変更してみる。

すると、次のようなエラーになる

  • postsのtitleがnullの場合
{
  "errors":[
    {
    "message": #Connect return null for non-nullable field Post.title. ",
      "locations": [
      {
        "line": 4,
        "colum": 5
      }
     ],
     "path": [
      "posts",
      0,
      "title" 
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
          ]
         }
        }
      }
    ],
    "data" {
      "posts": [
         null
      {
        "id": "2",
        "title": "こんばんは"
      }
      ]
    }  
  }

これは、GraphiQLの実行環境がスキーマと比較しレスポンスが妥当か検証してくれることによる。
APIサーバーのテストでクエリをエンドツーエンドで実行しておくとこのエラーをより発見しやすくなる

GraphQLはスキーを仕様として持っており、自然にスキーマから書き始められる点がスキーマ駆動開発と親和性が高い理由の一つ

これまで記事をまとめてみて

いろいろなぞってみて、まだ自分の中で落とし込むにはもう少し時間が必要だなーと。
情報をひっぱるとき、あんまり良くないことだと思いますがDBの値を変更して抽出する場合、その仕様はどこに記述しておけばよいのかまだ自分の中で分かっておりません。

また、別に用意しておくのかな?


  1. Javaのフレームワークの一つ。Spring Frameworkベースのアプリケーションを手軽に作成することができる。Spring Frameworkはフレームワークの中でも多くのエンジニアによって支持されている。 
  2. RSpec とは、Ruby における BDD (behavior driven development、ビヘイビア駆動開発) のためのテストフレームワークです。BDDはテストコードを、自然言語を用いて要求仕様のように (Spec = 仕様) 記述するための手法。 
  3. json-schema.orgによって開発されているJSON objectの設計書のようなもの 
%d人のブロガーが「いいね」をつけました。