「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の値を変更して抽出する場合、その仕様はどこに記述しておけばよいのかまだ自分の中で分かっておりません。
また、別に用意しておくのかな?