はじめに
OpenAPI定義ファイルが大きくなりすぎる問題は大規模システム開発でAPI設計をする際によく直面する課題です。
この問題に対処するための方法を紹介します。
OpenAPIとは
Webアプリケーション同士の安全な通信のために、どのような項目・形式で仕様を記載すべきか定義したフォーマットのことです。Excelなどで管理していたAPI仕様書をOpenAPIを用いて作成することで、フォーマットが統一されて管理しやすくなります。
サマリ
- ファイルの分割OpenAPI定義を複数のファイルに分割する
- 外部参照の
$ref
を使用して外部ファイルを参照する - 繰り返し使用される要素をcomponentsセクションで別ファイルに切り出し
- 外部参照の
エラーハンドリングやステータスコードの共通化しておくと、オープンAPI定義からのコード生成の省力化のメリットを享受できます。
ファイルの分割、共通化の検討は段階的に行うことが推奨されますが、その中でもAPIステータスコードの切り出しを優先して考えると良いでしょう。
ディレクトリ構造のサンプル
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)は再利用可能で、複数のパスやオペレーションから参照されます
-
ファイルの記述サンプル
3つの主要なファイルをサンプルとして、具体的な記述イメージを共有します。
-
main.yaml:APIの全体構造を定義
- 外部ファイル(paths, schemas, parameters, responses)への参照を含みます
サーバー情報とAPIのバージョンを指定します
- 外部ファイル(paths, schemas, parameters, responses)への参照を含みます
-
errors.yaml:共通のエラーレスポンスを定義
- 各種HTTPエラーステータス(400, 401, 403, 404, 429, 500など)に対応するレスポンスを含みます
レート制限エラー(429)に対して詳細な情報を提供します。
- 各種HTTPエラーステータス(400, 401, 403, 404, 429, 500など)に対応するレスポンスを含みます
-
success.yaml:レスポンス成功時のレスポンスを定義
- 標準的な成功レスポンス(200 OK, 201 Created, 204 No Content)を含みます
サンプル記述
main.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
# 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
# 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行を超えるサイズになります。
1ファイル300行程度に留めるようにしておくと、Github CopilotなどのAIコードアシスタントのサポートも享受できます。(ファイルサイズが大きいとよいサジェスチョンが出ない)