OpenAPI定義のファイルが大きくなりすぎる問題の対処


# はじめに

OpenAPI定義ファイルが大きくなりすぎる問題は大規模システム開発でAPI設計をする際によく直面する課題です。

この問題に対処するための方法を紹介します。

:::note info
**OpenAPIとは**
Webアプリケーション同士の安全な通信のために、どのような項目・形式で仕様を記載すべきか定義したフォーマットのことです。Excelなどで管理していたAPI仕様書をOpenAPIを用いて作成することで、フォーマットが統一されて管理しやすくなります。
:::


## サマリ 

- ファイルの分割OpenAPI定義を複数のファイルに分割する
    - 外部参照の`$ref`を使用して外部ファイルを参照する
    - 繰り返し使用される要素をcomponentsセクションで別ファイルに切り出し

エラーハンドリングやステータスコードの共通化しておくと、オープンAPI定義からのコード生成の省力化のメリットを享受できます。

ファイルの分割、共通化の検討は段階的に行うことが推奨されますが、その中でもAPIステータスコードの切り出しを優先して考えると良いでしょう。

## ディレクトリ構造のサンプル

```markdown

api-docs/
├── main.yaml                 # メインのOpenAPI定義ファイル
├── paths/                    # エンドポイントの定義
　　### PJごとに定義される業務領域
│   ├── users.yaml            # ユーザー関連のパス
│   ├── products.yaml         # 製品関連のパス
│   └── orders.yaml           # 注文関連のパス
│
│
├── components/               # 再利用可能なコンポーネント

　　### PJごとに定義される業務領域
　   ├── schemas/              # データモデルのスキーマ
　   │   ├── user.yaml         # ユーザースキーマ
　   │   ├── product.yaml      # 製品スキーマ
　   │   └── order.yaml        # 注文スキーマ

　　### P方式設計である程度共通化　+ 一部 PJごとに定義変更/追加
　   ├── parameters/           # 共通パラメータ
　   │   ├── pagination.yaml   # ページネーションパラメータ
　   │   └── filters.yaml      # フィルタリングパラメータ

　　### 方式設計である程度共通化　+ 一部 PJごとに定義変更/追加
　   ├── responses/            # 共通レスポンス
　   │   ├── errors.yaml       # エラーレスポンス
　   │   └── success.yaml      # 成功レスポンス

　　### 方式設計である程度共通化　+ 一部 PJごとに定義変更/追加
　   └── security-schemes/     # セキュリティスキーム
　       └── oauth2.yaml       # OAuth2設定
```

## 各ファイルに定義されること

それぞれの各ファイルに定義されることを表にまとめます。

### 分割と整理

- `main.yaml`は他のすべてのファイルを統合するエントリーポイントとして機能します
    - `$ref`を使用して、これらの共通要素を参照可能しています
    - `paths/`: 各リソースのエンドポイントを個別のファイルに分割
        - コンポーネント（schemas, parameters, responses）は再利用可能で、複数のパスやオペレーションから参照されます


# ファイルの記述サンプル

３つの主要なファイルをサンプルとして、具体的な記述イメージを共有します。

- **main.yaml:APIの全体構造を定義**
    - 外部ファイル（paths, schemas, parameters, responses）への参照を含みます
    サーバー情報とAPIのバージョンを指定します

- **errors.yaml:共通のエラーレスポンスを定義**
    - 各種HTTPエラーステータス（400, 401, 403, 404, 429, 500など）に対応するレスポンスを含みます
    レート制限エラー（429）に対して詳細な情報を提供します。

- **success.yaml:レスポンス成功時のレスポンスを定義**
    - 標準的な成功レスポンス（200 OK, 201 Created, 204 No Content）を含みます


# サンプル記述

### main.yaml

```yaml
openapi: 3.0.0
info:
  title: サンプルAPI
  version: 1.0.0
  description: これはサンプルAPIの定義です。

servers:
  - url: <https://api.example.com/v1>

tags:
  - name: users
    description: ユーザー関連の操作
  - name: products
    description: 製品関連の操作

paths:
  /users:
    $ref: './paths/users.yaml'
  /products:
    $ref: './paths/products.yaml'

components:
  schemas:
    User:
      $ref: './components/schemas/user.yaml'
    Product:
      $ref: './components/schemas/product.yaml'

  parameters:
    PaginationParams:
      $ref: './components/parameters/pagination.yaml'

  responses:
    BadRequestError:
      $ref: './components/responses/errors.yaml#/BadRequestError'
    UnauthorizedError:
      $ref: './components/responses/errors.yaml#/UnauthorizedError'
    ForbiddenError:
      $ref: './components/responses/errors.yaml#/ForbiddenError'
    NotFoundError:
      $ref: './components/responses/errors.yaml#/NotFoundError'
    RateLimitExceededError:
      $ref: './components/responses/errors.yaml#/RateLimitExceededError'
    InternalServerError:
      $ref: './components/responses/errors.yaml#/InternalServerError'
    OkResponse:
      $ref: './components/responses/success.yaml#/OkResponse'
    CreatedResponse:
      $ref: './components/responses/success.yaml#/CreatedResponse'
    NoContentResponse:
      $ref: './components/responses/success.yaml#/NoContentResponse'
    PaginatedResponse:
      $ref: './components/responses/success.yaml#/PaginatedResponse'

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

security:
  - BearerAuth: []

```

### errors.yaml

```errors.yaml
# components/responses/errors.yaml

# エラーレスポンス定義
responses:

  BadRequestError:
    description: リクエストが不正です
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: BAD_REQUEST
          message: リクエストパラメータが不正です

  InvalidArgumentError:
    description: クライアントが無効な引数を指定しました
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: INVALID_ARGUMENT
          message: リクエスト フィールド x.y.z は xxx です。[yyy、zzz] のいずれかが必要です。

  FailedPreconditionError:
    description: 現在のシステム状態ではリクエストを実行できません
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: FAILED_PRECONDITION
          message: リソース xxx は空でないディレクトリであるため、削除することはできません。

  OutOfRangeError:
    description: クライアントが無効な範囲を指定しました
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: OUT_OF_RANGE
          message: パラメータ「age」は [0、125] の範囲外です。

  UnauthorizedError:
    description: 認証に失敗しました
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: UNAUTHORIZED
          message: 認証に失敗しました

  UnauthenticatedError:
    description: リクエストが認証されませんでした
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: UNAUTHENTICATED
          message: 無効な認証情報。

  ForbiddenError:
    description: アクセス権限がありません
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: FORBIDDEN
          message: このリソースにアクセスする権限がありません

  PermissionDeniedError:
    description: クライアントに十分な権限がありません
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: PERMISSION_DENIED
          message: 許可「xxx」がリソース「yyy」に対して拒否されました。

  NotFoundError:
    description: リソースが見つかりません
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: NOT_FOUND
          message: リソース「xxx」が見つかりません。

  AbortedError:
    description: 同時実行の競合が発生しました
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: ABORTED
          message: リソース「xxx」のロックを取得できませんでした。

  AlreadyExistsError:
    description: リソースがすでに存在します
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: ALREADY_EXISTS
          message: リソース「xxx」はすでに存在します。

  RateLimitExceededError:
    description: APIリクエスト制限を超過しました
    headers:
      X-RateLimit-Limit:
        schema:
          type: integer
        description: 制限期間内の最大リクエスト数
      X-RateLimit-Remaining:
        schema:
          type: integer
        description: 現在の期間内で残っているリクエスト数
      X-RateLimit-Reset:
        schema:
          type: integer
        description: 制限がリセットされる時間（UNIXタイムスタンプ）
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/RateLimitError'
        example:
          code: RATE_LIMIT_EXCEEDED
          message: APIリクエスト制限を超過しました
          details:
            retryAfter: 60
            limit: 100
            remaining: 0
            reset: 1619788800

  ResourceExhaustedError:
    description: リソース割り当てが不足しているか、レート制限に達しています
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: RESOURCE_EXHAUSTED
          message: 割り当て制限「xxx」を超えました。

  CancelledError:
    description: リクエストはクライアントによってキャンセルされました
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: CANCELLED
          message: リクエストはクライアントによってキャンセルされました。

  DataLossError:
    description: 復元できないデータ損失またはデータ破損が発生しました
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: DATA_LOSS
          message: データ損失が発生しました。詳細はサーバーログを確認してください。

  UnknownError:
    description: 不明なサーバーエラーが発生しました
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: UNKNOWN
          message: 不明なエラーが発生しました。詳細はサーバーログを確認してください。

  InternalServerError:
    description: サーバー内部でエラーが発生しました
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: INTERNAL_SERVER_ERROR
          message: 内部サーバーエラーが発生しました。詳細はサーバーログを確認してください。

  NotImplementedError:
    description: API メソッドはサーバーによって実装されていません
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: NOT_IMPLEMENTED
          message: メソッド「xxx」は実装されていません。

  UnavailableError:
    description: サービスが利用できません
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: UNAVAILABLE
          message: サービスが一時的に利用できません。後ほど再試行してください。

  DeadlineExceededError:
    description: リクエスト期限を超えました
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        example:
          code: DEADLINE_EXCEEDED
          message: リクエストの処理に時間がかかりすぎました。後ほど再試行してください。

components:
  schemas:
    SuccessResponse:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: 成功コード
        message:
          type: string
          description: 成功メッセージ

    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: エラーコード
        message:
          type: string
          description: エラーメッセージ
        details:
          type: object
          description: 追加のエラー詳細情報

    RateLimitError:
      type: object
      required:
        - code
        - message
        - details
      properties:
        code:
          type: string
          description: エラーコード
        message:
          type: string
          description: エラーメッセージ
        details:
          type: object
          required:
            - retryAfter
            - limit
            - remaining
            - reset
          properties:
            retryAfter:
              type: integer
              description: リクエストを再試行するまでの待機時間（秒）
            limit:
              type: integer
              description: 制限期間内の最大リクエスト数
            remaining:
              type: integer
              description: 現在の期間内で残っているリクエスト数
            reset:
              type: integer
              description: 制限がリセットされる時間（UNIXタイムスタンプ）

```
### success.yaml

```success.yaml

# components/responses/success.yaml

OkResponse:
  description: リクエストが成功しました
  content:
    application/json:
      schema:
        $ref: '#/components/schemas/SuccessResponse'

CreatedResponse:
  description: リソースが正常に作成されました
  content:
    application/json:
      schema:
        $ref: '#/components/schemas/SuccessResponse'

NoContentResponse:
  description: リクエストが成功し、返すコンテンツがありません

PaginatedResponse:
  description: ページネーション付きのデータリスト
  content:
    application/json:
      schema:
        $ref: '#/components/schemas/PaginatedResult'

components:
  schemas:
    SuccessResponse:
      type: object
      required:
        - data
      properties:
        data:
          type: object
          description: レスポンスデータ
        message:
          type: string
          description: 成功メッセージ（オプショナル）

    PaginatedResult:
      type: object
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            type: object
          description: データの配列
        pagination:
          type: object
          properties:
            totalItems:
              type: integer
              description: 全アイテム数
            currentPage:
              type: integer
              description: 現在のページ番号
            pageSize:
              type: integer
              description: 1ページあたりのアイテム数
            totalPages:
              type: integer
              description: 総ページ数

```

# おわりに

OpenAPI定義ファイルが大きくなりすぎる問題への対処としてのファイル分割を検討しました。

OpenAPI定義を充分にするとモックの自動生成や、設計書の出力などのメリットを享受できます。ただし、充分な定義を行おうとするとファイルはすぐに1000行を超えるサイズになります。

１ファイル300行程度に留めるようにしておくと、Github CopilotなどのAIコードアシスタントのサポートも享受できます。(ファイルサイズが大きいとよいサジェスチョンが出ない)