はじめに
テクノロジー戦略本部の早瀬です。
RESTful な API の仕様の定義する時に OpenAPI を使用することはよくあると思います。
ですがある程度の規模のプロジェクトになってくるとopenapi.yaml
が肥大化して数千、数万行になってしまい下記のようなつらみが出てきます。
- 複数人での開発でコンフリクトが発生しやすい
- 目的の API の定義箇所が見つけにくい
- 記載方法が統一されておらず秩序がなくなる
$ref
がネストしてると探すのが大変- エディタによっては重すぎて開けない
ずらずら書きましたが要は、とにかく開発しずらいということです笑 そこで今回は治安がよく、できるだけストレスフリーな OpenAPI の開発環境を作っていこうと思います! 最終的なコードはこちらにプッシュしてあります。
openapi.yaml の分割
まずはopenapi.yaml
を分割して定義できるようにします。
OpenAPI では$ref
キーワードを使用することで定義済みのリソースを参照することができるのですが、別ファイルでリソースを定義して、そのファイルパスを$ref
で指定することで OpenAPI を分割して定義することができます。
paths
とschema
を分割して定義する場合のディレクトリ構成は下記のようになります。
├── paths # pathの定義ファイルを置く │ ├── task.yaml │ └── tasks.yaml ├── schemas # schemaの定義ファイルを置く │ ├── Error.yaml │ ├── GetTasksResponse.yaml │ └── Task.yaml ├── openapi.yaml └── Makefile
大元のファイルはopenapi.yaml
で、その中で各ファイルを参照している形になります。
paths
を別ファイルで定義することによって、openapi.yaml
では OpenAPI に関する基本情報とどんなパスがあるかしか記載されなくなるので記述量をかなり減らすことができます!
# openapi.yaml openapi: 3.0.0 info: title: OpenAPI Template description: Template of split OpenAPI with validator version: 1.0.0 servers: - url: "{server}" description: URL of the server variables: server: default: http://localhost:3000 tags: - name: task paths: /tasks: $ref: ./paths/tasks.yaml /task/{id}: $ref: ./paths/task.yaml
paths
別ファイルで定義したpaths
の内容は下記のようになります。
# paths/task.yaml get: tags: - task summary: Find task by id description: Return a single Task operationId: getTaskById parameters: - name: id in: path description: task id required: true schema: type: integer responses: 200: description: Successful operation content: application/json: schema: $ref: ../schemas/Task.yaml example: id: 1 name: prepare documents is_completed: false deadline: 2021/09/01
レスポンスのスキーマでTask.yaml
を参照していること以外特に特に変わったことはしてないですね。
そのパスに関する API の仕様をこのファイルに記載していけば OK です。
parameters
やresponses
も抽出して別ファイルに定義することは可能なのですが、ここではあえて別ファイルで定義せず記載しています。(理由に関しては後述します)
schemas
task.yaml
から参照していた Task.yaml は下記のようになります。
# schemas/Task.yaml type: object properties: id: type: integer description: ID of the task example: 1 name: type: string description: Name of the task example: prepare documents is_completed: type: boolean description: Whether the task is completed example: false deadline: type: string description: Deadline of the task example: 2021/09/01
ここでも特に変わったことはなく、ただスキーマを定義をしているだけですね。
レスポンスのスキーマ定義などで同じ階層の他のスキーマを参照したい場合は、そのまま指定してあげれば OK です!
スキーマの定義ファイルが多くなりそうな時は、API の種類やグループごとにschemas
配下にディレクトリを作成して管理してもいいと思います。
# schemas/GetTasksResponse.yaml type: object properties: tasks: type: array items: $ref: Task.yaml # 同じ階層のTask.yamlを参照
parameters や responses を分けない理由
先ほどparameters
やresponses
も別ファイルで定義可能ですが、今回はあえてそうしていないです。
意図としては分割しすぎてもわかりづらくなるかなと思ったからです。
分割すること自体が目的ではなくあくまで開発しやすくするのが目的なので、API に関する情報が一つのファイル内(今回の例だとpaths/task.yaml
)に収まっている方が個人的にはいいかなと思いました。
ただ openapi-generator などを使用してコードを生成する場合などは、定義の仕方によって生成されるコードが変わると思うので必要に応じて分割してもいいと思います。
レスポンスに関してはエラー時のレスポンスなどは共通のことが多いので、分けて定義してもいいかもしれません。
スタブサーバーの生成
OpenAPI を定義して何をしたいかはプロジェクトによると思うのですが、今回はopenapi-generatorを使用して分割して定義した OpenAPI からスタブサーバーを生成したいと思います。
ファイル指定にopenapi.yaml
を指定すれば勝手に参照を解決していい感じにしてくれます。
下記の make コマンドでスタブサーバーを生成からビルド、起動まで行えます。 Makefile 全体はこちらになります。
# Makefile DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) # generate the server stub .PHONY: generate_server_stub generate_server_stub: make clean_server_stub mkdir -p $(DIR)/ServerStub docker run --rm \ -v $(DIR):/local \ -e JAVA_OPTS="-Dlog.level=warn" \ openapitools/openapi-generator-cli generate \ -i /local/openapi.yaml \ -g spring \ -o /local/ServerStub \ --additional-properties returnSuccessCode=true,serverPort=8080 # build the server stub .PHONY: build_server_stub build_server_stub: docker run --rm \ -v $(DIR)/ServerStub:/local \ -v ~/.m2:/root/.m2 \ -w /local \ maven mvn package # run the server stub .PHONY: run run: docker run --rm \ -v $(DIR)/ServerStub:/local \ -w /local \ -p 3000:8080 \ openjdk java -jar target/openapi-spring-1.0.0.jar
1ファイルにまとめたい場合は下記コマンドで統合したファイルを生成できます。
docker run --rm \ -v $(DIR):/local \ openapitools/openapi-generator-cli generate \ -g openapi-yaml \ -i /local/openapi.yaml \ -o /local/generated/
openapi-validator の導入
次に openapi-validator を導入して記法のチェックを行うようにします。 下記 make コマンドでバリデーションを行うようにしています。
# Makefile validate_openapi: docker run --rm \ -v $(DIR):/local \ jamescooke/openapi-validator \ --verbose \ --report_statistics \ --config /local/validaterc.json \ /local/openapi.yaml
validaterc.json
にルールが定義してあり、必要に応じてルールを変更することができます。
さらに上記のバリデーションを CI に組み込んでいきます。 やはり秩序を保つにはコーディング規約が自動的、かつ強制的に守られるような仕組みにしてしまうのが一番です。 今回は github action を使用します。
# .github/workflows/ci.yaml name: CI on: push: pull_request: types: - opened workflow_dispatch: jobs: validate-openapi: name: Validate OpenAPI runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Validate openapi.yaml run: make validate_openapi
バリデーションに引っかかった場合はちゃんと CI が落ちてますね!
まとめ
openapi.yaml
を分割してバリデーターを導入したことで多少は開発しやすい環境ができたのではないでしょうか。
ファイルを分割するだけでもコンフリクトは減ると思いますし、何がどこにあるかがわかりやすくなると思います!
最後にバイセルテクノロジーズではエンジニアを募集しています! ご興味を持っていただけた方はぜひご応募ください! herp.careers