ドキュメントちゃんと保守できてますか?
API開発とドキュメントの保守は切っても切れない問題です。
仕様の記述はもちろんのこと、サンプルを試せるAPIクライアントや、仕様に則った実装になっているかテストも自動化したいですよね。
本記事では、現在開発中のAPIアプリケーションで、実際に僕が試行錯誤していく中でたどり着いたベストプラクティスを紹介しようと思います。
アーキテクチャ
- iOSアプリのバックエンドとしてJSONを返すAPIサーバー
- Rails6 × MySQL5.7 on Docker
いつもの というお買い物アプリです。
ドキュメント何で書いてますか?
- Excel => つらい
- Markdown => つらい
- 何らかのDSLを用いて生成するツール =>
素でマークダウンを書くのはつらみが深いので何かしらツールを使いましょう。
apiary, api blueprint, APIDOC, etc.
いろいろありますが、僕が激しくおすすめするのは OpenAPI です。
理由としては、
- Linux Foundation、Google、IBM、Microsoftなどが協力して仕様の策定に関わっていること
- 歴史が長く周辺ツールが豊富で、拡張性があること
- 表現力豊かな記述方法
が挙げられます。
Swagger? OpenAPI?
もともと Swagger という名前だったものが、 OpenAPIと名前を変えてバージョン3.0がリリースされました。
Swaggerと聞けば馴染みのある方も多いと思います。
基本的にSwaggerとOpenAPIを読み替えても問題はないのですが、(ドメインとか残ってるし => https://swagger.io/specification/)
Swaggerは2.xまでで、OpenAPIは3.0からになるので、Swaggerのバージョン3というものは厳密には存在しません。
OpenAPIが優れている理由
使ったことがない人向けに説明をしておくと、
OpenAPI とは、RESTful API を記述するためのフォーマットの標準であり、
その標準を用いて様々なことを解決するためのヘルパーツール群を提供するものです。
ツールは大きく3種類に分けられます。
-
Swagger Codegen
- スタブサーバーとクライアントSDKの生成
-
Swagger Editor
- 定義ファイルの編集が行えるリッチエディタ
- シンタックスのチェックや補完、ホットリロードでのプレビューをサポート
-
Swagger UI
- HTMLとしてドキュメントをビジュアライズする
- 定義されたホストに対してリクエストを送信するAPIクライアントとしても使用できる(Postman的な)
この3つのオープンソースツールを拡張する形で、各言語ごとのラッパーやフレームワークへの組み込みをサポートするライブラリが数多く作成されています。(ex. swagger-blocks)
自分が実現したいことに合わせて柔軟にモジュールを組み込むことができるので、どのプロジェクトにも導入がしやすいです。
また、先に述べたように、OpenAPI とはただ単にフォーマットを標準化した仕様
なので、
ツールを通さなくてもその仕様に則って記述した yaml ファイルをただ共有するだけみたいな使い方もできます。
yamlさえあれば、受け取った人は何かのツールを使って自由に拡張利用できるので、非常にポータビリティが高いと言えます。Dockerファイルで環境のやりとりするみたいなイメージに近い
そして錚々たる大企業たちがスポンサーとなっているため、業界の標準となっていくことは確実です。
OpenAPIの記述に慣れておくことは、エンジニアとして必要なスキルになってくるかと思っています。
実際に使ってみよう
今回実現したいことはこちらです。
- ドキュメントをブラウザで手軽に確認したい
- ドキュメントを楽に記述したい
- ドキュメントをローカルサーバーのHTTPクライアントとして使いたい
- レスポンスがドキュメントに則っているか自動テストしたい
これ全てOpenAPIでできます。
ただ実際にやろうとするとツールが豊富で選択肢が多い割に、3.0に対応していないものが多かったり、情報がまとまっていなかったり、ベストプラクティスにたどり着くまでに苦労したので、この記事が何かの助けになれば幸いです。
ちなみにですが、2.xと3.xでは破壊的な変更があるので、2系のツールで3系を動かすのは無理があります。
多くのツールで3に対応するissueが上がっているのですが、長い間放置されているものが多いため、3を使おうとすると選択肢は結構狭まります。
ドキュメントをブラウザで手軽に確認したい
まずはサンプルとなるエンドポイントを実装します。
Rails.application.routes.draw do
resources :users
end
class UsersController < ApplicationController
def index
user = {
:name => "sakuraya",
:age => 26
}
render :json => user
end
end
$ curl localhost:3000/users
{"name":"sakuraya","age":26}
これを OpenAPI のドキュメントとして記述します。
yaml と json がサポートされていますが、特に理由がなければ yaml を使うことをお勧めします。
ファイル名は何でもいいんですが、 openapi.yml
がスタンダードです。
openapi: 3.0.2
info:
title: "ぼくのかんがえたさいきょうのAPIドキュメント運用"
description: "サンプルアプリ"
version: "1.0.0"
tags:
- name: "users"
description: "ユーザーAPI"
paths:
/users:
get:
summary: "ユーザーを取得"
description: "ユーザーを取得"
tags:
- "users"
responses:
200:
description: "成功時"
content:
application/json:
schema:
type: "object"
properties:
name:
description: "名前"
type: "string"
example: "sakuraya"
age:
description: "年齢"
type: "integer"
example: 26
required:
- "name"
- "age"
最低限これだけ書けばOKです。
これをブラウザで見るためには、SwaggerUIというツールを使います。
SwaggerUIで調べると Node.js を使ってサーバーを立ち上げる例ばかりが出てきますが、
別で立てるのは面倒なので docker-compose で一緒に立ち上げてしまいます。
イメージが公開されているのでこれをベースにします。
version: '3'
services:
web: &app_base
build:
context: .
ports:
- 3000:3000
command: bundle exec rails s -p 3000 -b 0.0.0.0
volumes:
- .:/myapp
- bundle:/usr/local/bundle
depends_on:
- db
db:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: password
TZ: Asia/Tokyo
command: mysqld --character-set-server=utf8 --collation-server=utf8_general_ci
volumes:
- ./vendor/docker/db/data:/var/lib/mysql
- ./vendor/docker/db/conf.d:/etc/mysql/conf.d
- ./vendor/docker/db/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
ports:
- 3306:3306
doc:
image: swaggerapi/swagger-ui
volumes:
- ./doc/openapi.yml:/usr/share/nginx/html/openapi.yml
environment:
API_URL: openapi.yml
ports:
- 8080:8080
volumes:
bundle:
doc
の部分が SwaggerUI です。
doc
配下に置いたドキュメントファイルをマウントして、ファイル名を環境変数で指定することで、ドキュメントサーバーが立ち上がるようになっています。
これで http://localhost:8080 にアクセスすると、インタラクティブなUIでドキュメントが表示されます。
▼それぞれ対応するセクションがこうなっている。
▼パスをクリックするとアコーディオンが開いて仕様が表示される。
▼上の画像は Example Value
を表示したもので、 Schema
をクリックすると、プロパティの説明、型、requiredの有無、nullableの有無など詳細が表示される。
ドキュメントサーバーとAPIサーバーの立ち上げをいっぺんに管理できるのが便利
ドキュメントを楽に記述したい
エディタごとにプラグインがそれぞれあるかと思います。
僕は普段 VS Code を使っているのでこれを入れています。
あとはプレビュー用として Swagger Viewer を入れておいてもいいかと思います。
ホットリロードで確認しながらできるので便利です。
一つだけ注意点があって、たまに表示がおかしくなったり、正しく表示されないことがあります($ref
が展開されないなど)。
なので僕は書き方に慣れるまではこれを使っていましたが、いまはブラウザでまとめてチェックしています。
あとは書き方のTipsですが、 components
を使って共通化していくと見通しが良くなります。
paths:
/users:
get:
summary: "ユーザーを取得"
description: "ユーザーを取得"
tags:
- "users"
responses:
200:
description: "成功時"
content:
application/json:
schema:
$ref: "#/components/schemas/User"
components:
schemas:
User:
description: "ユーザー"
type: "object"
properties:
name:
description: "名前"
type: "string"
example: "sakuraya"
age:
description: "年齢"
type: "integer"
example: 26
required:
- "name"
- "age"
ドキュメントをローカルサーバーのHTTPクライアントとして使いたい
せっかく docker-compose してるんだから、ローカルのサーバーに実際につないで動かしたいですよね?
API開発時のHTTPクライアントにはずっと Postman や、
最近だと REST Client なんかを使っていましたが、ドキュメントの変更に対する反映が面倒だったりするのが難点です。
OpenAPIを組み込めればそんな手間もなくなります。
SwaggerUI には Try it out
というボタンがついています。
このままでは利用できないので設定を行います。
servers
というセクションを追記してください。
servers:
- url: "http://localhost:3000"
description: "local api server"
これがボタンを押した時のリクエスト先のベースURLとなります。
▼トップに servers
の設定が反映されました。複数設定することができ、切り替えられるようになっています。
▼ボタンを押すとパラメータが編集できるようになり、 Execute
ボタンが現れます。
(今回はパラメータないが適当に書くとこんな感じ。よしなにクエリパラメータに突っ込んでくれる。)
もう一つ設定が必要で、このままリクエストを送ってもこのようなエラーが出ます。
Access to fetch at 'http://localhost:3000/users?some_condition=true' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.Access to fetch at 'http://localhost:3000/users?some_condition=true' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Cross Origin の制約を回避するために、Rails側に設定が必要です。
rack-cors という gem を使います。
ドキュメントサーバーを使うのは開発環境だけなので、 development.rb
に記述します。
config.middleware.insert_before 0, Rack::Cors do
allow do
origins "localhost:8080"
resource "*", :headers => :any, :methods => :any
end
end
これでサーバーを再起動すれば、リクエストできるようになります。
▼生成されたcurlコマンドと、レスポンスが表示されました。
いちいち Postman 開いてリクエスト書く必要がないので幸せ
レスポンスがドキュメントに沿っているか自動テストしたい
これだけでもだいぶ開発が捗るようになったんですが、テストまでいきたいです。
ドキュメントで Integer になってるプロパティが String で返ってきたり、
required なプロパティがレスポンスになかったら落ちるようにしたいですよね。
rspecに組み込んでみます。
gem 'rspec-rails'
docker-compose run web bundle
docker-compose run web rails generate rspec:install
require "rails_helper"
RSpec.describe UsersController, :type => :request do
describe "#index" do
let(:path) { users_path }
it do
get path
expect(JSON.parse(response.body)).to match(
# いい感じにドキュメントのスキーマを検証したい
)
end
end
end
committee という gem を使います。
さらにそれを Rails 用にラップした committee-rails も使います。
gem 'committee'
gem 'committee-rails'
設定を追加。
config.add_setting :committee_options
config.committee_options = { :schema_path => Rails.root.join("doc", "openapi.yml").to_s }
include ::Committee::Rails::Test::Methods
テストを落とすために、 Integer であるはずの年齢を String に書き換えます。
class UsersController < ApplicationController
def index
user = {
:name => "sakuraya",
:age => "26"
}
render :json => user
end
end
テストをこう書きます。
require "rails_helper"
RSpec.describe UsersController, :type => :request do
describe "#index" do
let(:path) { users_path }
it do
get path
assert_request_schema_confirm
assert_response_schema_confirm
end
end
end
実行すると。。。
F
Failures:
1) UsersController#index
Failure/Error: assert_response_schema_confirm
Committee::InvalidResponse:
#/components/schemas/User/properties/age expected integer, but received String: 26
# /usr/local/bundle/gems/committee-3.3.0/lib/committee/schema_validator/open_api_3/operation_wrapper.rb:35:in `rescue in validate_response_params'
# /usr/local/bundle/gems/committee-3.3.0/lib/committee/schema_validator/open_api_3/operation_wrapper.rb:30:in `validate_response_params'
# /usr/local/bundle/gems/committee-3.3.0/lib/committee/schema_validator/open_api_3/response_validator.rb:20:in `call'
# /usr/local/bundle/gems/committee-3.3.0/lib/committee/schema_validator/open_api_3.rb:38:in `response_validate'
# /usr/local/bundle/gems/committee-3.3.0/lib/committee/test/methods.rb:27:in `assert_response_schema_confirm'
# ./spec/requests/users_spec.rb:9:in `block (3 levels) in <top (required)>'
# ------------------
# --- Caused by: ---
# OpenAPIParser::ValidateError:
# #/components/schemas/User/properties/age expected integer, but received String: 26
# /usr/local/bundle/gems/openapi_parser-0.6.1/lib/openapi_parser/schema_validator.rb:62:in `validate_data'
Finished in 0.11266 seconds (files took 3.96 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/requests/users_spec.rb:6 # UsersController#index
無事落ちました!
▼requiredな値がないときはこうなったりします。
1) UsersController#index
Failure/Error: assert_response_schema_confirm
Committee::InvalidResponse:
#/components/schemas/User missing required parameters: age
テストまでかけるとか最高か
普段の開発フローとしては、まずドキュメントを記述して仕様をレビュー => テスト書く => 実装というドキュメント&テスト駆動開発でやっています。
まとめ
今回は既存のプロジェクトに後から組み込んだので使ってませんが、
ドキュメントからモックサーバーを立ち上げたり、コードを生成したりするツールも言語ごとにいろいろあります。
うまく活用してAPIドキュメント運用のつらみから解放されましょう
OpenAPI の記述方法については、OpenAPI-Specification を見ながら覚えていくのがオススメです。
[追記]
今回使ったサンプルコード