committee×OpenAPI×RailsでスキーマファーストなAPI開発
2019.12.01
committeeというgemとOpenAPIのスキーマを使ってRailsでスキーマファーストなAPI開発を試してみました。
弊社でWebアプリケーションを制作する場合、フロントエンドとバックエンドは別々に開発することが多いです。
フロントはほぼNuxt.jsですが、APIは誰が主導するかや求められるスピード感などでプロジェクトごとに変わってきます。
私が主導するプロジェクトである程度スピード感も求められる場合は主にRuby On Railsで開発をしています。(以下Railsで開発するという前提で話を進めます)
前提
課題感
フロントエンドとバックエンドのプロジェクトが別々かどうかはあまり問題ではありませんが、APIの実装を担当する人とフロントエンドでAPIの組み込みを担当する人が異なる場合はAPI仕様の認識合わせが一つ大きな問題となります。
組み込みを担当する人がAPI側のコードを読める人であったとしても、組み込みのたびにコードを読み解くことはしたいと思わないでしょうし、APIの実装を担当する人も新しいエンドポイントの追加やメンバーの参加のたびに説明に時間は割きたくないでしょう。
一般的な解決策としてはAPI仕様をドキュメント化することが考えられますが、ただドキュメント化するという考えだけではなかなかうまくいかないのではないでしょうか。
OpenAPIの活用
弊社ではAPI仕様のドキュメント化に OpenAPI を活用しています。
DSLなどからOpenAPI(またはSwagger)の定義ファイルを出力するライブラリもありますが、対応するバージョンがライブラリに依存してしまうことなどもあり個人的にはYAMLで直接書く方法をとっています。
API仕様を記述したYAMLファイルはリポジトリにPushした時点でCircleCIのArtifactsとしてSwagger UIを展開し、プロジェクトの参加者が閲覧できるようにしています。(どこに展開したとしてもこれを見てもらう意識付けは必要ですが😅)
それでもドキュメントを更新するのを忘れてしまった、ドキュメントを更新したけど実装と違ってしまっていた、などの問題は起こりえます。
スキーマファーストな開発
API仕様のドキュメントを書くタイミングがいつであれ、「ドキュメントを書く」という作業は地味ですし実装との関連性がないとどうしても気が重くなります。
なにかいい方法はないかと探していたところ、OpenAPIのスキーマをもとにAPIレスポンスのテストを書くことができる committee というgemを紹介する記事があり、使ってみたところなかなか良さそうだったので簡単に使い方の紹介とどう活用できるかを考えてみました。
committeeを使ったテストの例
細かいセットアップはここでは割愛しますが、例えば以下のようなAPI仕様を定義したとします。
openapi: 3.0.2
info:
title: example
version: '1'
servers:
- url: http://localhost:3000/{api_version}
description: local server
variables:
api_version:
default: 'v1'
enum:
- 'v1'
components:
schemas:
User:
type: object
description: user
required: [id, email, first_name, last_name]
properties:
id:
type: integer
description: user id
email:
type: string
format: email
description: email address
first_name:
type: string
description: first name
last_name:
type: string
description: last name
paths:
/users/{user_id}:
get:
tags: [user]
parameters:
- in: path
name: user_id
description: user id
required: true
schema:
type: integer
responses:
200:
description: ok
content:
application/json:
schema:
$ref: '#/components/schemas/User'
この例ではバージョニングできるようパスを切っていますが、設定は以下のように prefix
を追加することで対応可能です。committee-railsというgemを使うとセットアップが簡単にできます。
config.include Committee::Rails::Test::Methods
config.add_setting :committee_options
config.committee_options = {
schema_path: Rails.root.join('doc', 'openapi.yml').to_s,
prefix: '/v1'
}
GET /users/{user_id}
でユーザー情報を取得するAPIという想定なので、これをRSpecとcommitteeで検証するコードは以下のようになります。
RSpec.describe 'user api', type: :request do
let(:user) { create(:user) }
describe 'GET /users/:id' do
it 'success' do
get user_path(user.id)
expect(response).to have_http_status(:ok)
assert_response_schema_confirm
end
end
end
このテストでOpenAPIで記述した仕様と実際のレスポンスが合致しているかどうかを検証できるようになりました。(committee-railsのREADMEには assert_schema_conform
を使った例が記載されていますが、deprecationの警告が出ていたので assert_response_schema_confirm
を使っています)
スキーマを書くことから始める
OpenAPIで記述した仕様をもとにテストを書くことができるようになったわけですが、これを実際の開発フローでどう活用すると良いか考えてみると、
- APIの仕様を検討
- 仕様が決まったらOpenAPIに則ってドキュメントを書く
- ドキュメントをもとにしたテストを書く
- API実装
- 組み込みへパス
というのがいいのかなと思いました。
進め方としてはTDDと言えると思いますが、ポイントとしては1, 2の段階でバックエンドとフロントエンドでAPIの仕様について認識を合わせた上でベースとなるドキュメントを作ることかと思います。
一番最初のステップでスキーマが共通認識のもとにできていて、APIはそれを満たす形で実装されることが保障されれば、認識の齟齬による手戻りを減らすことができるのではないでしょうか。
というのを一人で考えていたので、チームで活用していけるかはこれから試していきたいと思っています。
→スキーマからクライアントを生成して組み込む方法はこちらで紹介しています。
余談:committeeで気になったところ
プロパティの検証
Userのスキーマ定義で required: [id, email, first_name, last_name]
としていますが、requiredとしないプロパティの有無は検証できません。(明らかに違うタイプを返すなどは検証できます)
仕様として違和感があるわけではないですが、これはプロパティの追加時に記述を忘れてしまうことがありそうだなと思いました。
また、仕様には記載していない created_at
や updated_at
がレスポンスに含まれていてもテストは通ってしまいます。
仕様に記述したプロパティが過不足ないかを簡単に検証できると良いなと思ったので、ドキュメントなどみつつ考えてみようかと思います。
ファイルの参照が解釈されない
OpenAPIやSwaggerのファイルを記述していくとよくあるつらい点は、内容に対してファイルが肥大化しやすいことだと思います。(エンドポイントはそんなにないのに数百行とか😇)
YAMLやJSONを独自ルールで分割して結合するタスクを実行するというのはよく見る方法ですが、OpenAPIのバージョン3ではファイルの参照が可能になっています。
paths:
/users:
$ref: '../resources/users.yaml'
/users/{userId}:
$ref: '../resources/users-by-id.yaml'
このように分割したファイルの参照がサポートされているので、追加で結合する処理を走らせたりする必要はありません。
しかし、committeeではファイルの参照が正しく解釈されないようでスキーマの検証には使えませんでした。
やりようはあるのかもしれませんが、結局の所一つのファイルに書き出してそれをcommitteeから参照することになりそうです。
→スキーマを分割して管理する方法についてこちらに書きました。