バイセル Tech Blog

バイセル Tech Blogは株式会社BuySellTechnologiesのエンジニア達が知見・発見を共有する技術ブログです。

治安のいいOpenAPIの開発環境を作る

はじめに

テクノロジー戦略本部の早瀬です。

RESTful な API の仕様の定義する時に OpenAPI を使用することはよくあると思います。 ですがある程度の規模のプロジェクトになってくるとopenapi.yamlが肥大化して数千、数万行になってしまい下記のようなつらみが出てきます。

  • 複数人での開発でコンフリクトが発生しやすい
  • 目的の API の定義箇所が見つけにくい
  • 記載方法が統一されておらず秩序がなくなる
  • $refがネストしてると探すのが大変
  • エディタによっては重すぎて開けない

ずらずら書きましたが要は、とにかく開発しずらいということです笑 そこで今回は治安がよく、できるだけストレスフリーな OpenAPI の開発環境を作っていこうと思います! 最終的なコードはこちらにプッシュしてあります。

openapi.yaml の分割

まずはopenapi.yamlを分割して定義できるようにします。 OpenAPI では$refキーワードを使用することで定義済みのリソースを参照することができるのですが、別ファイルでリソースを定義して、そのファイルパスを$refで指定することで OpenAPI を分割して定義することができます。

pathsschemaを分割して定義する場合のディレクトリ構成は下記のようになります。

├── 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 です。 parametersresponsesも抽出して別ファイルに定義することは可能なのですが、ここではあえて別ファイルで定義せず記載しています。(理由に関しては後述します)

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 を分けない理由

先ほどparametersresponsesも別ファイルで定義可能ですが、今回はあえてそうしていないです。 意図としては分割しすぎてもわかりづらくなるかなと思ったからです。 分割すること自体が目的ではなくあくまで開発しやすくするのが目的なので、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 が落ちてますね!

f:id:bst-tech:20210921064802p:plain
github action

まとめ

openapi.yamlを分割してバリデーターを導入したことで多少は開発しやすい環境ができたのではないでしょうか。 ファイルを分割するだけでもコンフリクトは減ると思いますし、何がどこにあるかがわかりやすくなると思います!

最後にバイセルテクノロジーズではエンジニアを募集しています! ご興味を持っていただけた方はぜひご応募ください!

hrmos.co