API 仕様書の役割
API 仕様書は、バックエンドとフロントエンド、あるいはサービス間の「契約書」です。 仕様書が存在しない・または不完全な状態では、次のような問題が頻発します。
- フロントエンドとバックエンドの実装が食い違い、結合テストで発覚して手戻りが発生する
- エラーレスポンスの形式が統一されておらず、クライアント側のエラーハンドリングが複雑化する
- 認証方式の変更を口頭で伝達した結果、一部のクライアントが旧方式のまま動き続ける
💡 API 仕様は「コードより先に書く」のが理想
API First 設計では、OpenAPI の仕様ファイルを先に書き、コードをそこから自動生成します。実装前に仕様の齟齬を検出できるため、手戻りコストが大幅に削減されます。
エンドポイント定義
各エンドポイントには次の情報を定義します。
| 定義項目 | 内容 | 例 |
|---|---|---|
| HTTP メソッド | GET / POST / PUT / PATCH / DELETE | GET |
| パス | リソースを表す URI | /orders/{orderId} |
| 概要(summary) | 1行の短い説明 | 注文詳細を取得する |
| 説明(description) | 詳細な説明・制約・注意事項 | キャンセル済み注文も含む。削除済みは404を返す。 |
| operationId | 一意の操作識別子(コード生成に使用) | getOrderById |
| タグ | グループ化のためのカテゴリ | orders |
REST の命名規則: リソース名は複数形の名詞(/orders)、操作は HTTP メソッドで表現します。
動詞を URI に含める(/getOrder)のは避けてください。
認証・認可
| 方式 | 概要 | 定義すべき内容 |
|---|---|---|
| Bearer Token(JWT) | Authorization ヘッダにトークンを付与 | トークン取得エンドポイント、有効期限、リフレッシュ方法 |
| API Key | ヘッダまたはクエリに API キーを付与 | キー名(例: X-API-Key)、取得方法、権限スコープ |
| OAuth 2.0 | 外部サービス認可 | 認可フロー(Authorization Code / Client Credentials)、スコープ一覧 |
| Basic 認証 | Base64 エンコードされた ID:Password | 非推奨の旨と代替手段を明記する |
⚠️ 認証が不要なエンドポイントも明示する
「認証必須」の記載がないエンドポイントは意図的に Public なのか、書き漏れなのか判断できません。security: [](認証不要)を明示的に記述してください。
リクエスト仕様
リクエストには パスパラメータ・クエリパラメータ・ヘッダ・ボディの4種類があります。
| 種別 | 定義すべき内容 |
|---|---|
パスパラメータ{id} |
名前・型(string / integer)・必須・説明・例・バリデーション(最大値・パターン等) |
クエリパラメータ?limit=20 |
名前・型・必須/任意・デフォルト値・許容範囲・説明 |
| リクエストヘッダ | ヘッダ名・必須/任意・説明(Content-Type, Authorization, Accept-Language 等) |
| リクエストボディ | Content-Type・JSON Schema(各フィールドの型・必須・バリデーション・example) |
レスポンス仕様
各 HTTP ステータスコードに対するレスポンスを定義します。
| ステータス | 意味 | どのエンドポイントに定義するか |
|---|---|---|
| 200 OK | 正常取得・更新成功 | GET / PUT / PATCH |
| 201 Created | リソース作成成功 | POST |
| 204 No Content | 削除成功(ボディなし) | DELETE |
| 400 Bad Request | リクエストパラメータ不正 | 全エンドポイント |
| 401 Unauthorized | 認証失敗 | 認証必須エンドポイント全て |
| 403 Forbidden | 権限不足 | 権限チェックがあるエンドポイント |
| 404 Not Found | リソースが存在しない | パスパラメータでリソース指定するエンドポイント |
| 409 Conflict | 重複リソースの作成など競合 | POST(一意制約あり) |
| 429 Too Many Requests | レート制限超過 | 全エンドポイント |
| 500 Internal Server Error | サーバ内部エラー | 全エンドポイント |
エラーレスポンス定義
エラーレスポンスのフォーマットをプロジェクト全体で統一します。RFC 7807(Problem Details for HTTP APIs)が業界標準です。
{
"type": "https://errors.example.com/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "リクエストパラメータに不備があります",
"instance": "/orders/create",
"errors": [
{
"field": "quantity",
"message": "1 以上の整数を指定してください",
"rejectedValue": -1
},
{
"field": "productId",
"message": "必須項目です",
"rejectedValue": null
}
],
"traceId": "550e8400-e29b-41d4-a716-446655440000"
}
| フィールド | 役割 | 必須 |
|---|---|---|
type | エラー種別を示す URI(ドキュメントへのリンク) | 推奨 |
title | エラーの短い概要 | 必須 |
status | HTTP ステータスコード(数値) | 必須 |
detail | 人間が読めるエラー詳細 | 推奨 |
instance | エラーが発生したリソースの URI | 任意 |
traceId | ログ追跡用の一意 ID | 推奨 |
レート制限・ページネーション
レート制限と ページネーションは仕様書で明示的に定義しないと、クライアント側が適切に実装できません。
| 定義項目 | 内容・例 |
|---|---|
| レート制限(制限値) | 100 リクエスト / 分 / API Key |
| レスポンスヘッダ | X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset |
| 429 時の Retry-After | Retry-After: 30(秒数)または日時 |
| ページネーション方式 | offset / cursor ベース |
| クエリパラメータ | limit(デフォルト20、最大100)/ offset または cursor |
| レスポンス | total(総件数)/ hasMore(次ページ有無)/ nextCursor |
OpenAPI 形式のテンプレート
openapi: "3.1.0"
info:
title: Order Service API
version: "2.3.0"
description: |
注文管理サービスの REST API 仕様書。
認証には Bearer Token(JWT)を使用する。
contact:
name: Backend Team
email: backend@example.com
license:
name: Private
servers:
- url: https://api.example.com/v2
description: 本番環境
- url: https://staging-api.example.com/v2
description: ステージング環境
security:
- bearerAuth: [] # デフォルトで全エンドポイントに認証を適用
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
Order:
type: object
required: [id, status, totalAmount, createdAt]
properties:
id:
type: string
format: uuid
example: "550e8400-e29b-41d4-a716-446655440000"
status:
type: string
enum: [pending, confirmed, shipped, delivered, cancelled]
totalAmount:
type: integer
description: 合計金額(税込み、円)
minimum: 0
createdAt:
type: string
format: date-time
Error:
type: object
required: [title, status, detail]
properties:
type:
type: string
format: uri
title:
type: string
status:
type: integer
detail:
type: string
traceId:
type: string
paths:
/orders:
get:
summary: 注文一覧を取得する
operationId: listOrders
tags: [orders]
parameters:
- name: limit
in: query
schema:
type: integer
default: 20
minimum: 1
maximum: 100
description: 1 ページの取得件数
- name: cursor
in: query
schema:
type: string
description: ページネーション用カーソル
- name: status
in: query
schema:
type: string
enum: [pending, confirmed, shipped, delivered, cancelled]
description: ステータスでフィルタ
responses:
"200":
description: 注文一覧取得成功
headers:
X-RateLimit-Limit:
schema:
type: integer
description: 1分あたりの上限リクエスト数
X-RateLimit-Remaining:
schema:
type: integer
description: 残りリクエスト数
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Order'
hasMore:
type: boolean
nextCursor:
type: string
nullable: true
"401":
description: 認証失敗
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
"429":
description: レート制限超過
headers:
Retry-After:
schema:
type: integer
description: 次のリクエストまでの待機秒数
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Python で API 仕様を自動検証する
① OpenAPI 仕様ファイルの整合性チェック
"""
openapi-spec-validator で仕様ファイルの構文・スキーマ整合性をチェックする。
インストール:
pip install openapi-spec-validator pyyaml
"""
import sys
import yaml
from openapi_spec_validator import OpenAPIV31SpecValidator
from openapi_spec_validator.exceptions import OpenAPISpecValidatorError
def validate_spec(path: str) -> None:
with open(path, encoding="utf-8") as f:
spec = yaml.safe_load(f)
validator = OpenAPIV31SpecValidator(spec)
errors = list(validator.iter_errors())
if not errors:
print(f"✅ {path} — 仕様ファイルに問題はありません")
# エンドポイント一覧を表示
paths = spec.get("paths", {})
print(f"\n登録エンドポイント数: {sum(len(v) for v in paths.values())}")
for path_key, methods in sorted(paths.items()):
for method, op in methods.items():
op_id = op.get("operationId", "-")
summary = op.get("summary", "-")
print(f" {method.upper():7} {path_key:<40} {op_id} — {summary}")
else:
print(f"❌ {path} — {len(errors)} 件のエラーが見つかりました\n")
for i, err in enumerate(errors, 1):
print(f" [{i}] {err.message}")
if err.path:
print(f" 場所: {' > '.join(str(p) for p in err.path)}")
sys.exit(1)
if __name__ == "__main__":
spec_file = sys.argv[1] if len(sys.argv) > 1 else "openapi.yaml"
validate_spec(spec_file)
② 実装と仕様のズレを検出(Flask 版)
"""
Flask アプリに登録されたルートと OpenAPI 仕様のエンドポイントを比較し、
「仕様にあるが実装がない」「実装にあるが仕様がない」エンドポイントを検出する。
インストール:
pip install pyyaml flask
"""
import re
import yaml
from pathlib import Path
def get_spec_endpoints(spec_path: str) -> set[tuple[str, str]]:
"""OpenAPI 仕様からエンドポイント(method, path)セットを返す。"""
spec = yaml.safe_load(Path(spec_path).read_text(encoding="utf-8"))
endpoints = set()
for path, methods in spec.get("paths", {}).items():
for method in methods:
if method.lower() not in ("get", "post", "put", "patch", "delete"):
continue
# OpenAPI の {id} 形式を Flask の 形式に変換
flask_path = re.sub(r"\{(\w+)\}", r"<\1>", path)
endpoints.add((method.upper(), flask_path))
return endpoints
def get_flask_endpoints(app) -> set[tuple[str, str]]:
"""Flask アプリからエンドポイントセットを返す。"""
endpoints = set()
for rule in app.url_map.iter_rules():
for method in rule.methods or []:
if method in ("HEAD", "OPTIONS"):
continue
endpoints.add((method, rule.rule))
return endpoints
if __name__ == "__main__":
# Flask アプリのインポート(実際の app モジュールに合わせて変更)
from app import create_app # type: ignore
flask_app = create_app()
spec_eps = get_spec_endpoints("openapi.yaml")
flask_eps = get_flask_endpoints(flask_app)
only_in_spec = spec_eps - flask_eps
only_in_flask = flask_eps - spec_eps
if only_in_spec:
print("⚠️ 仕様に定義があるが実装が存在しないエンドポイント:")
for method, path in sorted(only_in_spec):
print(f" {method:7} {path}")
if only_in_flask:
print("\n⚠️ 実装が存在するが仕様に定義がないエンドポイント:")
for method, path in sorted(only_in_flask):
print(f" {method:7} {path}")
if not only_in_spec and not only_in_flask:
print("✅ 仕様と実装のエンドポイントは一致しています")
③ API レスポンスのスキーマ自動検証
"""
schemathesis でステージング環境に対して OpenAPI 仕様に基づく
自動テストを実行し、仕様との不整合を検出する。
インストール:
pip install schemathesis
実行:
python -m schemathesis run openapi.yaml \
--url https://staging-api.example.com/v2 \
--auth-type bearer --auth "$TOKEN" \
--checks all
"""
# pytest との組み合わせ(CI 用)
import schemathesis
schema = schemathesis.from_path(
"openapi.yaml",
base_url="https://staging-api.example.com/v2",
)
@schema.parametrize()
def test_api(case):
"""仕様書に定義されたすべてのエンドポイントに対してランダムな値でリクエストを送り、
レスポンスが仕様のスキーマに準拠しているか検証する。"""
response = case.call()
case.validate_response(response)
まとめ
✅ API 仕様書で定義すべき要素
□ エンドポイント(HTTP メソッド・パス・概要・operationId・タグ)
□ 認証・認可(方式・トークン取得方法・スコープ・認証不要エンドポイントの明示)
□ リクエスト(パスパラメータ・クエリ・ヘッダ・ボディの型/必須/バリデーション)
□ レスポンス(全 HTTP ステータスのスキーマ・example)
□ エラーレスポンス形式(RFC 7807 準拠・traceId・field errors)
□ レート制限(制限値・レスポンスヘッダ・Retry-After)
□ ページネーション(方式・パラメータ・レスポンス構造)
□ バージョニング戦略(URI バージョン v1/v2 または Accept ヘッダ)