使えるドキュメントを作る:FastAPI×OpenAPI設計の実践――スキーマ設計、エラーモデル、例・例外、タグ整理、カスタムUI、クライアント自動生成
要約(全体像)
- OpenAPIを仕様の中心に据え、FastAPIの
response_model
やresponses
、examples
、schemas
を活用して、読みやすく矛盾のないAPIドキュメントを作る。 - 例外とエラー応答は共通スキーマで統一し、
HTTPException
とハンドラで確実に整形する。 - タグ、バージョン、サーバ情報、セキュリティ定義を整理して、利用者が迷わない構造にする。
- 仕上がったOpenAPIからクライアントSDKを自動生成し、品質を実装と同時に保つ。
- ドキュメントの拡張(説明・外観・多言語注釈)と**例(Examples)**の充実で、実装者・QA・フロントが合意を共有できる。
誰が読んで得をするか
- 学生エンジニアAさん:FastAPIでAPIを作ったが、ドキュメントの質が低く、利用者から質問が多い。例とエラーモデルの揃え方を学びたい。
- 小規模チームBさん:受託案件で外部公開APIを提供。OpenAPI整備とSDK自動生成でサポートコストを下げたい。
- SaaS開発Cさん:機能追加が早い。スキーマ・テスト・ドキュメントを連動させ、破壊的変更の管理を明確にしたい。
1. まず決めるべきこと:OpenAPIを設計の源泉に
OpenAPI(旧Swagger)はHTTP APIの機械可読・人間可読な仕様書。FastAPIは型注釈とPydantic(v2)から自動でOpenAPIを生成するため、正しいスキーマを記述するほどドキュメント品質が上がる。
判断ポイント:
- 入力(Body/Query/Path/Headers)と出力(
response_model
)を必ずモデル化する。 - 正常系だけでなく失敗系もスキーマ化し、コードから常に同じ形で返す。
- **例(Examples)**を充実させ、利用者が試しやすい状態にする。
2. スキーマの基本:response_model
と例(Examples)
2.1 入出力モデルの分離
# schemas/article.py
from pydantic import BaseModel, Field
from datetime import datetime
class ArticleCreate(BaseModel):
title: str = Field(..., max_length=120, description="記事タイトル")
body: str = Field(..., description="本文(Markdown可)")
class Article(BaseModel):
id: int
title: str
body: str
created_at: datetime
class Config:
from_attributes = True
2.2 エンドポイントでの適用と例
# routers/articles.py
from fastapi import APIRouter, HTTPException
from fastapi import status
from schemas.article import Article, ArticleCreate
router = APIRouter(prefix="/articles", tags=["articles"])
@router.post(
"",
response_model=Article,
status_code=status.HTTP_201_CREATED,
responses={
201: {
"description": "作成成功",
"content": {
"application/json": {
"examples": {
"ok": {
"summary": "正常例",
"value": {
"id": 101,
"title": "API設計の心得",
"body": "設計は仕様から逆算…",
"created_at": "2025-10-02T09:00:00Z"
}
}
}
}
},
}
},
)
async def create_article(payload: ArticleCreate):
# 実装は省略(DB保存など)
return Article(id=101, **payload.dict(), created_at="2025-10-02T09:00:00Z")
ポイント:
responses
に**例(examples)**を直接記述可能。response_model
により、自動ドキュメントとスキーマ整合が取れる。
3. 失敗系を揃える:エラーモデルと例外ハンドラ
3.1 共通エラースキーマ
# schemas/error.py
from pydantic import BaseModel, Field
class ErrorDetail(BaseModel):
code: str = Field(..., description="エラーコード(機械可読)")
message: str = Field(..., description="人間向け説明")
info: dict | None = Field(None, description="補足情報")
class ErrorResponse(BaseModel):
detail: ErrorDetail
3.2 ハンドラで統一整形
# main.py
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from schemas.error import ErrorResponse, ErrorDetail
class DomainError(Exception):
def __init__(self, code: str, message: str, info: dict | None = None):
self.code, self.message, self.info = code, message, info
app = FastAPI(title="OpenAPI Oriented API")
@app.exception_handler(DomainError)
async def handle_domain_error(_: Request, exc: DomainError):
payload = ErrorResponse(detail=ErrorDetail(code=exc.code, message=exc.message, info=exc.info))
return JSONResponse(status_code=400, content=payload.dict())
3.3 ドキュメントにエラー例を載せる
# routers/articles.py(一部)
from schemas.error import ErrorResponse
@router.get(
"/{article_id}",
response_model=Article,
responses={
404: {
"model": ErrorResponse,
"description": "見つからない場合",
"content": {
"application/json": {
"examples": {
"not_found": {
"summary": "存在しないID",
"value": {"detail": {"code": "ARTICLE_NOT_FOUND", "message": "記事がありません"}}
}
}
}
},
}
},
)
async def get_article(article_id: int):
# 実装は省略
raise DomainError("ARTICLE_NOT_FOUND", "記事がありません")
判断ポイント:
- 例外は1箇所で整形し、全エンドポイントで同じ形にそろえる。
responses
に失敗例も示して、利用者の想定を揃える。
4. タグ、説明、メタ情報の整理
4.1 タグと説明
app = FastAPI(
title="OpenAPI Oriented API",
description="仕様を中心にしたAPI。記事・ユーザー・認証の3ドメインで構成。",
version="1.2.0", # 仕様バージョン
openapi_tags=[
{"name": "articles", "description": "記事の作成・取得・更新・削除"},
{"name": "users", "description": "ユーザー管理"},
{"name": "auth", "description": "認証・トークン関連"},
],
)
4.2 サーバ・連絡先・ライセンス
app.openapi_servers = [
{"url": "https://api.example.com", "description": "本番"},
{"url": "https://stg-api.example.com", "description": "ステージング"},
]
app.openapi_contact = {"name": "APIサポート", "email": "support@example.com"}
app.openapi_license_info = {"name": "MIT"}
判断ポイント:
- タグは機能ドメインごとに分け、説明を短く添える。
- サーバや連絡先を明記し、利用者の迷いをなくす。
5. セキュリティ定義と使用法
5.1 セキュリティスキーム(Bearer)
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import Depends, HTTPException, status
bearer_scheme = HTTPBearer(auto_error=False)
def get_current_user(token: HTTPAuthorizationCredentials = Depends(bearer_scheme)):
if not token or token.scheme.lower() != "bearer":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="unauthorized")
# 検証は省略
return {"sub": "alice"}
# ルーター側
@router.get("/me", tags=["auth"])
async def me(user = Depends(get_current_user)):
return user
5.2 OpenAPIへの反映
FastAPIはHTTPBearer
などを使うとOpenAPIのsecuritySchemes
を自動生成。どのエンドポイントで必須かを、依存の有無で明確化できる。
判断ポイント:
- 認証が要るルートは必ず依存を追加し、仕様上も必須にする。
- スコープがある場合はOAuth2スキームを選択。
6. responses
の活用と再利用
応答仕様が増えると、同じ定義を何度も書きがち。関数や辞書で定義を再利用するとブレが減る。
from schemas.error import ErrorResponse
def error_responses(resource: str):
return {
400: {"model": ErrorResponse, "description": "入力エラー"},
404: {
"model": ErrorResponse,
"description": f"{resource} が見つからない",
},
409: {"model": ErrorResponse, "description": "重複や競合"},
}
@router.put(
"/{article_id}",
response_model=Article,
responses=error_responses("記事")
)
async def update_article(article_id: int, payload: ArticleCreate):
...
判断ポイント:
- 語彙の統一(同じ現象は同じ
code
・説明)で学習コストを下げる。 - 返せない応答を書かない。正直な仕様にする。
7. 例(Examples)を増やして使いやすく
- 正常例:最小・代表・境界(空要素・最大長)を1つずつ。
- 失敗例:典型的なバリデーションエラー、認可エラー、競合。
- 説明:
summary
と短いdescription
で背景を添える。
Tips:
- 長い例はJSONファイルに分離し、読み込みで再利用しても良い。
- Swagger UI上の「試してみる」が成功しやすいほど、問い合わせが減る。
8. OpenAPIをエクスポートし、クライアントSDKを生成
8.1 エクスポート
# 生成(アプリ起動不要のパスも可)
python -c "import json; import uvicorn; \
from app.main import app; \
print(json.dumps(app.openapi(), ensure_ascii=False))" > openapi.json
または起動後に /openapi.json
を取得。
8.2 SDK自動生成(例:OpenAPI Generator)
# TypeScript Axiosクライアント
openapi-generator generate \
-i openapi.json \
-g typescript-axios \
-o client-ts
他にもpython
, kotlin
, swift
, go
など多数。生成後は型安全な呼び出しができ、仕様変更も差分で追える。
判断ポイント:
- SDK生成はCIで自動化し、破壊的変更があるとPRで目に見えるようにする。
- 生成物は別リポジトリに切り出すと配布が楽。
9. バージョニングと非互換変更の扱い
- パスでのバージョン:
/api/v1
→ 将来/api/v2
を並走。旧版は告知期間を設けて段階的に廃止。 - スキーマの互換性:既存フィールドの削除や型変更は非互換。新規フィールド追加は多くの場合後方互換。
- 変更ログ:OpenAPIと実装、SDKのリリースノートを同じ単語で記述し、追跡可能にする。
判断ポイント:
- 破壊的変更は新バージョンのパスで隔離。
- 移行ガイドをドキュメントに含め、期限を明記。
10. ドキュメントの外観・メタ強化
- タイトル・説明・タグを簡潔に、初めての人が迷わない導入文をつける。
- 重要な制約(レート制限、サイズ上限、タイムゾーン、日時形式)をトップにまとめる。
/docs
(Swagger UI)と/redoc
の両方を提供。ReDocは記述が読みやすい長文向け。
11. サンプル:一気通貫の最小API骨格
# main.py
from fastapi import FastAPI
from routers.articles import router as article_router
app = FastAPI(
title="OpenAPI Oriented API",
description="仕様を中心に設計したサンプル",
version="1.2.0",
openapi_tags=[
{"name": "articles", "description": "記事機能"},
{"name": "auth", "description": "認証"},
],
)
app.include_router(article_router, prefix="/api/v1")
@app.get("/health", tags=["meta"])
def health():
return {"ok": True}
仕様の読みやすさが実装品質とサポートコストに直結する。小さく始めて、例とエラーモデルから整えるのが近道。
12. テストでスキーマの破綻を早期検知
- OpenAPI JSONをCIで生成→検証(スキーマリンター・ツールの利用)。
- 代表的なエンドポイントをスナップショットテストで固定(
responses
・例の差分検知)。 - バリデーションエラーの形が変わっていないか、型と必須項目を継続的に確認。
13. よくある落とし穴と対策
症状 | 原因 | 対策 |
---|---|---|
実装とドキュメントがズレる | 手書きドキュメント | 自動生成を前提に、responses ・例をコードに集約 |
失敗系がバラバラ | 例外を直返し | 共通エラーモデル+ハンドラで統一 |
使い方が伝わらない | 例が不足 | Examplesを各応答に用意、境界条件も示す |
認証の仕様が不明確 | スキーム未定義 | securitySchemes が出る依存を追加し、必須範囲を明記 |
破壊的変更で混乱 | バージョン未分離 | **/api/v2 **で隔離、移行ガイドと期限を明記 |
14. 導入ロードマップ
response_model
と共通エラーモデルを導入(正常・失敗の基本形を固定)。- 主要エンドポイントにExamplesを追加し、Swagger UIから試せる形に。
- タグ・説明・サーバ情報を整理し、導入文と制約の一覧をトップに。
- OpenAPIのエクスポートとSDK自動生成をCIに組み込む。
- 破壊的変更は新バージョンで段階移行。
参考リンク
- FastAPI
- OpenAPI
- Pydantic
- SDK生成
まとめ
- 入出力と失敗系をモデル化し、例外は共通エラーモデルでそろえる。
responses
とExamplesで試しやすい仕様に。タグ・説明・セキュリティ・サーバ情報を整理する。- OpenAPIをエクスポートしてクライアントSDKを自動生成し、仕様と実装を同じ線路に乗せる。
- 破壊的変更はバージョン分離と移行ガイドで混乱を避ける。今日から、使えるドキュメントづくりを始めましょう。