CTO兼福岡オフィス立ち上げ担当として新アプリを作っている@edvakfです。
JSON APIを開発しているとこういう問題がありがちですよね。
- 仕様どおりにAPIの形式を作ったはずだけどなんか自信が持てない
- テストでいくつかのキーが存在するかの簡単なチェックはしてるつもりだけど、全部チェックするのは大変すぎる
- APIのControllerやViewをリファクタリングしたらレスポンスの形が変わってアプリがめっちゃクラッシュし始めた
- というのが怖くて誰もリファクタリングできなくなった
- APIドキュメントがメンテされない
- 知らない間にレスポンスのフィールドが増えてたけどドキュメントに書いてない
これらを解決したい!と思って試行錯誤したら、スマートに解決することができました。この記事ではRailsのことについて書きますが、考え方は他の言語・フレームワークでも同じです。
なお、今回使ったgemのバージョンはこちらです。
- rails 5.1.4
- swagger_blocks 2.0.1
- apivore 1.6.2
- swagger_ui_engine 1.1.0
Swagger
Swaggerの説明はウェブ上に山ほどあるので説明は端折りますが、JSON APIの仕様を定義するための仕様です。Swaggerの仕様にしたがってYAMLを書けば、Swagger-UIというウェブの画面でAPIドキュメントが見れたり、パラメータを変えてリクエストを投げてみることができるのが便利です。
ピクシブでは2015年に僕がScalaのサービスでSwaggerを導入して以来、RailsでもPHPでも、スマホアプリ用のAPIはほぼ必ずSwaggerを使っています。
クライアントサイドの人がリクエストパラメータを確認する用途が主なので、 リクエストパラメータ部分は丁寧に記述しても、レスポンス形式は労力に見合わないので記述していない ケースが多いです。
RailsアプリではGrape-SwaggerのアプリとSwagger::Blocksのアプリがあります。僕は以前はGrape-Swaggerを使っていましたが、今作っているアプリではSwagger::Blocksを使い始めました。ControllerやViewの書き方が普通のRailsチックになることや、気軽に導入していざとなれば簡単にやめられるのが良いところです。
Apivore
ApivoreはSwaggerの生成するJSONに対してAPIレスポンスの形式が正しいかを検証してくれるテストツールです。
こういうテストを書くと、Swaggerの定義に基づいてレスポンスのJSONに対して型チェックをしてくれます。
これの良いところは
- Swaggerの定義のすべてのエンドポイントをテスト しなければならない
- 下の方の
expect(subject).to validate_all_paths
による
- 下の方の
- Swaggerの定義ファイル(
/swagger.json
)はRailsアプリから配信されていれば良いので、Grape-SwaggerでもSwagger::Blocksでも直書きのYAMLでもよく、定義ファイルを作るツールを選ばない
などです。
結果
Apivoreを導入したことにより、 レスポンス形式もきちんと記述するのモチベーション が生まれました。むしろお釣りが来るほどのリターンが得られます。
具体的には、
- jbuilderを大幅にリファクタリングしてもテストが通っていればまあ大丈夫だろうと自信を持てるようになった
- APIのコードレビューの時に人間が注意するべきことが格段に減った
- LLの感覚でなんとなく定義していたAPIを、静的型付き言語のように型と構造体を意識して書かないといけなくなった
- リファクタリングやテストの過程で、そういえば未定義だった動作とか、似たような構造なのにちょっと違うみたいなクライアント泣かせな箇所が見つかった
- クライアント側の人に「このAPIの型定義はSwagger-UIを見といてください」と言えるようになった
というメリットが得られました。
そもそもAPIというものは一度仕様をバッチリと決めてしまえば、そうそう変わることはないのです。一回定義とテストを書いておくことで長期の将来に渡って安全性を担保できるというのは、控えめに言っても超お得です。
ピクシブ福岡オフィスでスピーディーな開発サイクルを体験したい人向けの求人情報です。
おまけ
Apivoreを使うためのSwagger::Blocksの設定例
先程の subject { Apivore::SwaggerChecker.instance_for('/swagger.json') }
でいうところの /swagger.json
というパスに、JSONを生成するエンドポイントを作ってあげなければいけません。routes.rbにこう書いて、 resources :swagger, only: [:index]
Controllerを作ります。
Swagger::Blocksの swagger_path
などのブロックは各APIを実装するControllerに書いても良いですが、散らばっても煩雑になるので、 このSwaggerControllerに全部並べて書く あるいは定義が増えてきたら concernsに書いてこのSwaggerControllerからincludeする をおすすめします。そうすると上の例にある SWAGGERED_CLASSES
は未来永劫 [self]
のみで良いです。
additionalProperties
Swaggerでオブジェクトを定義するときは additionalProperties: false
をつけておくのが良いです。
これをつけると、未定義のフィールドが見つかった時にApivoreのテストで失敗してくれるようになります。すべてのフィールドの定義を書いたつもりだけど抜けてたとか、将来新たなフィールドを追加した時に定義を修正し忘れるということが無くなるので、さらに堅牢性が高まります。
SwaggerUiEngineの設定例
SwaggerUiEngineはSwagger-UIをMountable Engineとして使えるgemです。これの設定にも先程のJSONを指定します。
今回導入したApivoreもSwagger::BlocksもSwaggerUiEngineも、お互いが独立していて、乗っかっているRailsアプリにちょこっとくっつけるタイプのライブラリなので、その気になれば簡単に捨てることができるのも大きな利点です。
↓ピクシブ福岡オフィスの求人情報です。