green snake
Photo by Pixabay on Pexels.com

使えるドキュメントを作る:FastAPI×OpenAPI設計の実践――スキーマ設計、エラーモデル、例・例外、タグ整理、カスタムUI、クライアント自動生成


要約(全体像)

  • OpenAPIを仕様の中心に据え、FastAPIのresponse_modelresponsesexamplesschemasを活用して、読みやすく矛盾のない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. 導入ロードマップ

  1. response_model共通エラーモデルを導入(正常・失敗の基本形を固定)。
  2. 主要エンドポイントにExamplesを追加し、Swagger UIから試せる形に。
  3. タグ・説明・サーバ情報を整理し、導入文と制約の一覧をトップに。
  4. OpenAPIのエクスポートとSDK自動生成をCIに組み込む。
  5. 破壊的変更は新バージョンで段階移行。

参考リンク


まとめ

  • 入出力と失敗系をモデル化し、例外は共通エラーモデルでそろえる。
  • responsesExamplesで試しやすい仕様に。タグ・説明・セキュリティ・サーバ情報を整理する。
  • OpenAPIをエクスポートしてクライアントSDKを自動生成し、仕様と実装を同じ線路に乗せる。
  • 破壊的変更はバージョン分離と移行ガイドで混乱を避ける。今日から、使えるドキュメントづくりを始めましょう。

投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)