銀座Rails #40 に登壇してきました。
登壇資料資料はこちら
以下は資料をテキストにしたものです。
目次
PR
UZUMAKIではアジャイル開発で新規事業の開発から、大規模Webアプリケーションのアーキテクチャ更新のなど開発をしています。
お問い合わせはUZUMAKIのHPのお問合せフォームから
OpenAPIを利用する要件
OpenAPIを使う要件を分解してみよう
- メンテナブルなAPI設計書をつくりたい
- モックサーバを使ってスキーマ駆動開発がしたい
- TypeScriptのAPIアクセス部分を自動生成したい
メンテナブルなAPI設計書をつくりたい
- API設計書を自動生成してドキュメント作成コストを下げたい
- ソースコードとドキュメントの二重管理を避けたい
- 手動で作るとだんだんメンテナンスされなくなる
- この規模のサービスでもドキュメントない/メンテされてないの!?なんてことあるよね?
参考: How to use OpenAPI3 for API developer
モックサーバを使ってスキーマ駆動開発がしたい
- OpenAPI version3に対応したメジャーなモックサーバ
使い方はとても簡単
> prism mock doc/openapi.yaml
レスポンスはopenapi.yamlの各メソッドのexampleを返してくれる
TypeScriptのAPIアクセス部分を自動生成したい
- OpenAPIから各種言語のフロントエンドの自動生成
- openapi-generator
- Rubyも生成できるし、Swift, Kotlin, Dartも生成できる
- TypeScriptの生成も様々なタイプができる Angular, Aurelia, Axios, Fetch, Inversify, jQuery, Nestjs, Node, redux-query, Rxjs)
生成コマンド(axios対応のTypeScriptの場合)
> openapi-generator-cli generate -i \\
./YOUR_PATH/openapi.yaml -g typescript-axios -o openapi/types \\
--additional-properties=modelPropertyNaming=camelCase,supportsES6=true
自動生成されたコード例
async getShops() {
state.isLoading = true
const { data, error }: any = await new ApiV1ShopApi(
undefined,
$config.baseURL,
axios as AxiosInstance
)
.apiV1ShopsGet(1, 1000)
.catch((error) => {
return { error }
})
state.isLoading = false
if (error) {
state.error = error
return
}
state.shops = data.shops
},
各ライブラリの特徴と要件を満たすか
RailsでOpenAPIでググるとこの3つよく出る
- Grape
- committee-rails
- rspec-openapi
※ 以降生成されるOpenAPIのファイルopenapi.yamlを OpenAPI定義ファイル表記します
Grape
RailsのAPIモードが出る前まで、RailsでAPIといえばこれ
Grape: https://github.com/ruby-grape/grape
現在ではあまり人気のないDSL形式でAPIを作成できる OpenAPIもRailsが動的に作成してくれる
Grapeのメリット、デメリット
メリット
- grape-entity
- レスポンスで返すモデルで表示させる属性を明示的にできる
- OpenAPIのレスポンスのコンポーネントを自動で作成してくれる
- grape-swagger
- OpenAPI定義ファイルを動的に生成、さらにSwaggerUIもついてくるオールインワン
- 先人の日本語情報が比較的多い
Grapeのメリット、デメリット
デメリット
- DSLのため学習コストがかかる
- スキーマを先に作って実装する駆動開発ができない
- grape-swaggerがOpenAPIのv2にしか対応されておらずv3のブランチがずっと本体にされていない
Grapeが要件を満たすか
- メンテナブルなAPI設計書をつくりたい
- 🔺 SwaggerUIもv2なので機能が少ない
- モックサーバを使ってスキーマ駆動開発がしたい
- ⭕ PrismはOpenAPI v2にも対応
- TypeScriptのAPIアクセス部分を自動生成したい
- 🔺 未確認、v2なのでだんだんメンテされなくなるだろう
committee-rails
committeeをrailsで組み込みやすくしてくれるgem
committeeはOpenAPI定義ファイルは自分で書く必要があるが、
その定義通りのリクエスト・レスポンスをテストできる assert_schema_conform
が便利
committee-rails: https://github.com/willnet/committee-rails
committee-railsのメリット、デメリット
メリット
- 純粋なスキーマ駆動開発ができる
- Prismでモックを動かせるのでRails側とフロント側で同時並行に実装できる
- 他2つと違ってRailsで入出力だけ作るなどからめ手が不要
- OpenAPIの定義どおりにRailsを実装できたかテストできる
committee-railsのメリット、デメリット
デメリット
- OpenAPI定義ファイルを手作業で書かなければならい
- OpenAPI定義ファイルだけ先に作るので他2つよりも実装に不安がある
- 特に初期は項目の追加など手戻りが頻発する状態でフロントの担当に共有していいものか悩ましい
OpenAPI定義ファイルを手作業で書かなければならい問題は
を使ってGUIで開発するとちょっと楽
committee-railsが要件を満たすか
- メンテナブルなAPI設計書をつくりたい
- ⭕ OpenAPIも最新のv3に対応
- モックサーバを使ってスキーマ駆動開発がしたい
- ⭕ PrismはOpenAPI v3に対応
- TypeScriptのAPIアクセス部分を自動生成したい
- ⭕ 正しいOpenAPIファイルなのできれいに生成する
rspec-openapi
Rspecのリクエストスペックを書けばOpenAPI定義ファイルを出力してくれる テスト駆動開発大好きな人に朗報
rspec-openapi: https://github.com/k0kubun/rspec-openapi
rspec-openapiのメリット、デメリット
メリット
- request specを書けば定義ファイルを作成してくれる
- 各APIのレスポンス例のexampleもテスト結果から作成してくれる(他2つにはない)
request spec例
it "hogehoge" do の箇所がOpenAPIのdescriptionにこの分が反映されるのでrspecのいつもの書き方をすると違和感が出やすいので注意
RSpec.describe 'Tables', type: :request do
describe '#index' do
it 'テーブルの配列を取得する' do
get '/tables', params: { page: '1', per: '10' }, headers: { authorization: 'k0kubun' }
expect(response.status).to eq(200)
end
it 'does not return tables if unauthorized' do
get '/tables'
expect(response.status).to eq(401)
end
end
# ...
end
生成されるOpenAPI定義ファイル例
openapi: 3.0.3
info:
title: rspec-openapi
paths:
"/tables":
get:
summary: index
tags:
- Table
parameters:
- name: page
in: query
schema:
type: integer
example: 1
- name: per
in: query
schema:
type: integer
example: 10
responses:
'200':
description: returns a list of tables
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: integer
name:
type: string
# ...
rspec-openapiのメリット、デメリット
デメリット
- 仕様を満たすOpenAPI定義ファイルを作るには、手動で対応するワークアラウンドが結構ある
※ 今年ぶつかった問題なので急に具体的になります
rspec-openapiのワークアラウンド
ログイン必須の場合は各メソッドの定義に手動で追記する必要がある
devise_token_authで認証用のgemを利用した場合 client, access_token, uidをヘッダにつける必要あり
各メソッドにつける
paths:
"/api/v1/shops":
get:
security:
- client: []
access_token: []
uid: []
認証情報の定義
components:
securitySchemes:
client:
type: apiKey
description: ClientID
in: header
name: client
access_token:
type: apiKey
description: access-token
in: header
name: access-token
uid:
type: apiKey
description: uid
in: header
name: uid
rspec-openapiのワークアラウンド
レスポンスがヘッダだけの場合不要な情報が付与されるので削除する必要がある
responses:
'204':
description: 商品カテゴリを更新
# これ以降を削除する
content:
'':
schema:
type: string
example: ''
responses:
'204':
description: 商品カテゴリを更新
rspec-openapiのワークアラウンド
specを実行時にIDがインクリメントされるとパラメタが追加されてしまう
letでidを指定してレコードを保存して実行すると、idがインクリメントされてparametersの内容が増えてしまうため自動生成時に同じ入力パラメータが重複してエラーになる
let(:category) { create(:category, name: '名前') }
IDを指定してやる
let(:category) { create(:category, id:1, name: '名前') }
rspec-openapiのワークアラウンド
コンポーネントを自分で作らなければならない
自動生成されたもの
responses:
'200':
description: 製品情報のリストを取得
content:
application/json:
schema:
properties:
products:
type: array
items:
type: object
properties:
id:
type: integer
name:
type: string
この状態だとコンポーネントを使わないとこのようなよろしくないクラス名になる
/**
*
* @export
* @interface InlineResponse2006
*/
export interface InlineResponse2006 {
/**
*
* @type {number}
* @memberof Shop
*/
id: number;
...
}
こういう意味の分かる名前のクラスを生成したい
/**
*
* @export
* @interface Shop
*/
export interface Shop {
/**
*
* @type {number}
* @memberof Shop
*/
id: number;
...
}
そのためOpenAPI定義ファイルに
responses:
'200':
description: お店のリスト取得する
content:
application/json:
schema:
properties:
products:
type: array
items:
"$ref": "#/components/schemas/Shop"
OpenAPI定義ファイルのコンポーネントの定義
components:
schemas:
Shop:
nullable: true
type: object
properties:
id:
type: integer
name:
type: string
レスポンスをコンポーネントに移動したあと 新しいメソッドを追加し、OpenAPI定義ファイルを自動生成すると コンポーネントと、レスポンスのオブジェクトが2つ存在してしまう。
つまりここうなってしまう
responses:
'200':
description: お店のリストを取得
content:
application/json:
schema:
properties:
products:
type: array
items:
"$ref": "#/components/schemas/Shop"
# せっかくコンポーネントを作ったのに下記の余分なものがついてしまう
type: object
properties:
id:
type: integer
name:
type: string
OpenAPI generatorでTypeScriptを生成する際にエラーになるため都度消す必要がある。
※ SwaggerUIやPrismを動かす分には先に書いてあるコンポーネントを利用するため気にならない
rspec-openapiが要件を満たすか
- メンテナブルなAPI設計書をつくりたい
- 🔺 最新のOpenAPI v3に対応するが、ワークアラウンドが多くAPIの本数が増えるとどんどん辛くなる
- モックサーバを使ってスキーマ駆動開発がしたい
- ⭕ PrismはOpenAPI v3に対応
- TypeScriptのAPIアクセス部分を自動生成したい
- 🔺 特にコンポーネント周りのワークアラウンドを実施しないと、自動で命名されたTypeScriptのクラスになってしまう
まとめ
今回の要件要件
- メンテナブルなAPI設計書をつくりたい
- モックサーバを使ってスキーマ駆動開発がしたい
- TypeScripのAPIアクセス部分を自動生成したい
これらすべての要件を満たす⭕のはcommittee-rails
まとめ
ただし開発の初期段階(1-3週間ぐらい)で、仕様が変わりやすい段階でcommittee-railsを使うと rspec-openapiで動作するRailsでOpenAPIを作成しつつ、 徐々に仕様が固まってAPIの本数が増えてきたらcommittee-railsに切り替えるのが良いのでないか
まとめ
実行時のOpenAPIの厳密さは右に行くほど適当な書き方だとエラーになる確率が上がる(体感) Swagger UI > Prism > Openapi Generator
そのためOpenAPIのファイルをコミットする場合は自動生成が通ってからコミットするのがコツ
PR
XではUZUMAKIの新しい働き方や日常の様子を紹介!ぜひフォローをお願いします!
noteではUZUMAKIのメンバー・クライアントインタビュー、福利厚生を紹介!
UZUMAKIではRailsエンジニアを絶賛募集中です。
↓の記事を読んでご興味を持っていただいた方は、ぜひ応募よろしくお願いします!
是非応募宜しくおねがいします!