every Tech Blog

株式会社エブリーのTech Blogです。

OpenAPI の定義を積極的にファイル分割して見通しを良くしてみる

この記事は every Tech Blog Advent Calendar 2024 1 日目の記事です。

はじめに

こんにちは、トモニテ開発部ソフトウェアエンジニア兼、CTO 室 Dev Enable グループの rymiyamoto です。
Advent Calendar のトップバッターを務めさせていただきます!

今回は OpenAPI でスキーマ駆動開発をしていく上での定義ファイルの管理方法についてお話できればと思います。

元々別の新規プロダクトで採用されていた分割方法をトモニテでも取り入れてみたので、その知見を共有します。

tech.every.tv

現状の管理方法からの問題点

過去の記事にてご紹介しましたが、トモニテのあるプロジェクトで oapi-codegen という OpenAPI の定義(YAML ファイル)からコードを生成できるツールを使ってドキュメント駆動開発を行っています。

tech.every.tv

最初のうちは YAML は大した大きさではなかったのですが、開発が進むにつれて YAML ファイルの行数が数千行に達し、特定の定義を探すのに時間がかかるようになってきてしまいました。

ある程度コンポーネントを定義してはいましたが、特定のレスポンスを修正するには 1 ファイル内をソース検索するのが都度手間になります。
そのため、各定義を細かくファイルに分割することができないか、メンバーが試行錯誤していました。

分割の手段

今回採用したのは redocly-cli というドキュメントを生成・管理するためのコマンドラインツールを使って、分割した定義ファイルを結合する方法です。

github.com

定義自体は現状以下のようにまとめています。

イメージ

openapi/
├── common # 共通の定義
│   ├── parameters.yml
│   ├── responses.yml
│   ├── schemas.yml
│   └── securitySchemes.yml
├── web # サービスごとの定義
│   ├── endpoints
│   │   ├── auth.yml
│   │   └── user.yml
│   ├── main.yml
│   ├── parameters
│   │   └── user.yml
│   ├── properties
│   │   └── auth.yml
│   ├── requestBodies
│   │   └── auth.yml
│   ├── responses
│   │   ├── auth.yml
│   │   └── user.yml
│   └── schemas
│       ├── auth.yml
│       └── user.yml
└── gen
    └── web.yml

common には共通の定義をまとめ、web にはサービスごとの定義をまとめています。

各ファイルは main.yml から参照される形になっています。

# main.yml
openapi: 3.1.0

paths:
  /auth:
    $ref: "endpoints/auth.yml#/paths/~1auth"
  /users:
    $ref: "endpoints/user.yml#/paths/~1users"

components:
  securitySchemes:
    Bearer:
      $ref: "../common/securitySchemes.yml#/components/securitySchemes/Bearer"

# endpoint/user.yml
paths:
  /users:
    get:
      operationId: GetUsers
      description: ユーザー一覧を返すAPI
      tags:
        - user
      parameters:
        - $ref: "../parameters/user.yml#/components/parameters/StatesUserParam"
        - $ref: "../../common/parameters.yml#/components/parameters/PageParam"
        - $ref: "../../common/parameters.yml#/components/parameters/PerPageParam"
      responses:
        "200":
          $ref: "../responses/user.yml#/components/responses/GetUsersResponse"
        "400":
          $ref: "../../common/responses.yml#/components/responses/ErrorResponse"
        # 以下略

# parameters/user.yml
components:
  parameters:
    StatesUserParam:
      in: query
      name: states
      description: "状態"
      required: false
      schema:
        type: array
        items:
          type: boolean
          example: true
      example: true

# responses/user.yml
components:
  responses:
    GetUsersResponse:
      description: ユーザー一覧
      content:
        application/json:
          schema:
            type: object
            additionalProperties: false
            properties:
              users:
                type: array
                description: ユーザーの配列
                items:
                  $ref: "../schemas/user.yml#/components/schemas/User"
              pagination:
                $ref: "../../common/schemas.yml#/components/schemas/Pagination"
            required:
              - users
              - pagination

# schemas/user.yml
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
          format: uint64
          x-go-name: "ID"
          description: "ID"
          example: 1
        email:
          type: string
          description: "メールアドレス"
          example: "test@every.tv"
        created_at:
          type: string
          description: "作成日時"
          example: "2023-01-01T00:00:00Z"
        updated_at:
          type: string
          description: "更新日時"
          example: "2023-01-01T00:00:00Z"
        deleted_at:
          type: string
          description: "削除日時"
          example: "2023-01-01T00:00:00Z"
      required:
        - id
        - email
        - created_at
        - updated_at

この定義上で redocly-cli を使って結合を行い、gen ディレクトリに結合した定義を出力することで、定義の管理を行っています。

docker run --rm -v $${PWD}/openapi:/spec redocly/cli:1.25.14 bundle web/main.yml -o gen/web.yml

あとは生成された gen/web.ymloapi-codegen でコード生成することで、コードとドキュメントを生成することができます。

また、POST や PUT などのフォームデータを送る場合の定義も同様に分割しています。

# endpoint/auth.yml
paths:
  /auth:
    post:
      operationId: Login
      description: ログインAPI
      tags:
        - auth
      requestBody:
        $ref: "../requestBodies/auth.yml#/components/requestBodies/LoginRequest"
      responses:
        "200":
          $ref: "../responses/auth.yml#/components/responses/LoginResponse"
        "400":
          $ref: "../../common/responses.yml#/components/responses/ErrorResponse"
        # 以下略

# requestBodies/auth.yml
components:
  requestBodies:
    LoginRequest:
      required: true
      content:
        application/json:
          schema:
            type: object
            description: ログイン
            properties:
              email:
                $ref: "../properties/auth.yml#/components/properties/LoginEmail"
              password:
                $ref: "../properties/auth.yml#/components/properties/LoginPassword"
            required:
              - email
              - password

# properties/auth.yml
components:
  properties:
    LoginEmail:
      name: email
      type: string
      description: "メールアドレス"
      example: "example@every.tv"
      x-oapi-codegen-extra-tags:
        validate: required,email,max=100 # go-playground/validator のタグを指定

分割によるメリット・デメリット

もちろん分割により各ドメイン単位でファイルが分けられるため、かなり見通しが良くなりました。
そのため、レビューでも差分の確認がしやすいかと思います。

ただ、今回極力分割をしてみましたが、新しい定義を追加するときには記載する箇所が多くなるため、人によってはかえってやりづらいと感じることもあると思います。
分割粒度は任意で決められるので、程よいポイントを見つけることが重要かと思います。

まとめ

今回は OpenAPI の定義ファイルを分割して管理する方法についてご紹介しました。

気づいたら肥大化してそのままになっている定義ファイルを分割することで、見通しを良くし、レビューもしやすくなりました。

一例に過ぎないので、各自のプロジェクトに合わせて適切な分割方法を見つけていただければと思います。

また、この方法を見つけてくれたメンバーに感謝 🙏 です。

最後に

エブリーでは、ともに働く仲間を募集しています。

テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください!

corp.every.tv

最後までお読みいただき、ありがとうございました!