FastAPIのOpenAPI活用術:Swagger UIを「仕様書」として育てるAPI設計(バージョニング・エラー設計・ページネーションまで)
先に要点(読む前のガイド)
- FastAPIはコードからOpenAPIスキーマを自動生成し、Swagger UI / ReDocで確認できます。これを「ただの自動ドキュメント」ではなく、チームの共通言語になる仕様書として育てるのが本記事のテーマです。
- OpenAPIを育てる鍵は、タグ・要約・説明・レスポンス・エラー形式・例(examples)を整え、エンドポイントの設計ルールを揃えることです。
- バージョニング(/v1など)と互換性の考え方、ページネーション、共通エラーモデル、レスポンスの一貫性といった「運用で効く設計」を、FastAPIの機能に落とし込みます。
- この記事を読み終えると、Swagger UIに「迷子にならないAPI」の輪郭が揃い、仕様変更やレビュー、利用者への案内がぐっと楽になります。
誰が読むと役に立つか(具体的に)
個人開発・学習者さん
小さなAPIを公開し始めて、「自分のためのメモ」が欲しくなってきた方に向いています。Swagger UIが整っていると、未来の自分が助かります。とくに、エラー形式やレスポンス形式を揃えるだけで、フロント実装がかなり楽になります。
小規模チームのバックエンドエンジニアさん
複数人で触るようになると、APIの設計が人によって揺れて、レビューや修正が増えがちです。タグ・エラー・ページネーションなどの基礎ルールをOpenAPIに反映すると、チームの合意形成がしやすくなります。
SaaS開発チーム・スタートアップの皆さま
外部連携(パートナー向けAPI、社内別チーム、モバイルアプリなど)が増えるほど、OpenAPIの品質はそのまま開発速度に跳ね返ります。バージョニングや互換性、仕様の明確化は「後からやるほど高くつく」ので、早めに型を作っておくと安心です。
アクセシビリティ評価(読みやすさの配慮)
- 最初に要点を箇条書きで提示し、途中でも「何を達成したい章か」が見出しだけで追えるようにしています。
- 専門用語(OpenAPI、スキーマ、互換性など)は初出で短く説明し、以降は用語を統一して混乱を減らしています。
- コード例は短めに分割し、1ブロックに詰め込みすぎないようにしています。
- 章ごとに独立して読めるよう、必要な前提はその章内で補っています。
全体として、技術記事としての読みやすさと理解の段階性を意識し、WCAG AA相当を目標に構成しています。
1. OpenAPIは「自動で出るからOK」ではなく「育てる資産」
OpenAPIは、APIの仕様を機械可読な形(JSON/YAML)で表現する標準です。FastAPIはこれを自動生成し、Swagger UIやReDocで表示できます。
ただ、最初の状態は「動くことは分かるけれど、意図が伝わりにくい」ことが多いです。たとえば、
- エンドポイントの並びが雑然として探しにくい
- パラメータの意味が分からない
- エラーがどんな形で返るのか不明
- 成功レスポンスの例がなく、実装側が迷う
こういう状況だと、API利用者(将来の自分も含みます)が「結局コードを読む」ことになり、仕様書としての価値が薄れます。
逆に、Swagger UIを見れば、
- このAPIは何をするのか
- どういう入力が必要か
- 成功すると何が返るか
- 失敗するとどうなるか
- 互換性はどう守るか
が分かる状態にしておくと、レビューも実装も運用もぐっと楽になります。
2. まず整える:タイトル・説明・タグ・ルーター分割
2.1 APIのメタ情報を入れる
FastAPI() の引数で、ドキュメントに出る情報を整えます。
from fastapi import FastAPI
app = FastAPI(
title="Example API",
description="FastAPIで作るサンプルAPI。OpenAPIを仕様書として育てる前提の構成です。",
version="1.0.0",
contact={"name": "Support", "email": "support@example.com"},
license_info={"name": "MIT"},
)
この情報は外部公開時に信用にもつながりますし、チーム内でも「このAPIは何のためのものか」が分かりやすくなります。
2.2 タグで分類する(Swagger UIが探しやすくなる)
ルーター側で tags を揃えておくと、UIでカテゴリ分けされます。
from fastapi import APIRouter
router = APIRouter(prefix="/users", tags=["users"])
タグ名は「名詞」「小文字」「複数形」など、チームでルールを決めると揺れが減ります。たとえば users, articles, auth, admin のように粒度を揃えると見通しが良いです。
2.3 ルーター分割とバージョニングの土台
このあとバージョニングの話をしますが、先に土台を置いておきます。
from fastapi import FastAPI
from app.api.v1.routers import users, articles
app = FastAPI(title="Example API", version="1.0.0")
app.include_router(users.router, prefix="/v1")
app.include_router(articles.router, prefix="/v1")
/v1 をprefixにしておくと、互換性の管理がやりやすくなります。
3. 仕様書としての品質を上げる:summary・description・response_model
3.1 エンドポイントに「要約」と「補足」を書く
FastAPIでは、デコレータ引数でAPI説明を補強できます。
from fastapi import APIRouter
from app.schemas import UserRead
router = APIRouter(prefix="/users", tags=["users"])
@router.get(
"",
summary="ユーザー一覧を取得",
description="管理画面向けのユーザー一覧を返します。検索・ページネーションに対応します。",
response_model=list[UserRead],
)
def list_users():
...
summary は短く、description はもう少し丁寧に。UIではこれがそのまま「読むべき情報」になります。
3.2 response_modelで「返す形」を固定する
レスポンスの型を response_model で明示すると、スキーマが整い、利用者が迷いにくくなります。
from pydantic import BaseModel
class UserRead(BaseModel):
id: int
name: str
email: str
response_model がないと、実装が返した辞書の形がそのままになり、将来的に破壊的変更が混ざりやすくなります。反対に response_model を通すと、不要なフィールドを落とすなどのコントロールも効きます。
4. 例(examples)を入れる:利用者の迷いを一気に減らす
Swagger UIで一番助かるのは、実は「例」です。入力と出力の例があるだけで、理解が早まります。
4.1 リクエストボディの例
Pydanticモデルの json_schema_extra(v2系)を使うと、例を入れられます。
from pydantic import BaseModel, Field
class UserCreate(BaseModel):
name: str = Field(..., description="表示名")
email: str = Field(..., description="メールアドレス")
model_config = {
"json_schema_extra": {
"examples": [
{"name": "山田花子", "email": "hanako@example.com"}
]
}
}
4.2 レスポンスの例(複数ケース)
エンドポイント側でも responses で例を示せます。
from fastapi import APIRouter, status
from app.schemas import UserRead
router = APIRouter(prefix="/users", tags=["users"])
@router.post(
"",
summary="ユーザーを作成",
response_model=UserRead,
status_code=status.HTTP_201_CREATED,
responses={
201: {
"description": "作成成功",
"content": {
"application/json": {
"example": {"id": 1, "name": "山田花子", "email": "hanako@example.com"}
}
},
}
},
)
def create_user():
...
例は過剰に増やす必要はありません。利用者が一番迷いやすい「入力」「成功」「よくある失敗」だけで十分に価値があります。
5. エラー設計:共通エラーフォーマットでAPIの使い勝手が変わる
API利用者が一番困るのは「失敗したときにどう扱えばいいか分からない」状態です。エラーを統一すると、フロント実装やクライアントSDKが一気に書きやすくなります。
5.1 共通エラーモデルを作る
ここでは、よくある形として次のようなモデルを用意します。
from pydantic import BaseModel
from typing import Any
class ErrorDetail(BaseModel):
code: str
message: str
detail: dict[str, Any] | None = None
class ErrorResponse(BaseModel):
error: ErrorDetail
codeは機械判定用(例:USER_NOT_FOUND)messageは人間向け短文detailは任意(例:バリデーションエラーのフィールド名など)
5.2 代表的なエラーをresponsesに載せる
from fastapi import APIRouter, HTTPException
from app.schemas import UserRead, ErrorResponse
router = APIRouter(prefix="/users", tags=["users"])
@router.get(
"/{user_id}",
summary="ユーザー詳細を取得",
response_model=UserRead,
responses={
404: {
"model": ErrorResponse,
"description": "ユーザーが存在しない",
"content": {
"application/json": {
"example": {
"error": {
"code": "USER_NOT_FOUND",
"message": "ユーザーが見つかりません",
"detail": {"user_id": 999}
}
}
}
}
}
},
)
def get_user(user_id: int):
user = None
if not user:
raise HTTPException(status_code=404, detail="not found")
return user
このままだと HTTPException の detail がそのまま出るので、実務では「例外ハンドラ」で統一フォーマットに整形するのがおすすめです。
5.3 例外ハンドラでエラーを統一する
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from app.schemas import ErrorResponse
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
code = "HTTP_ERROR"
if exc.status_code == 404:
code = "NOT_FOUND"
body = ErrorResponse(error={"code": code, "message": str(exc.detail), "detail": None})
return JSONResponse(status_code=exc.status_code, content=body.model_dump())
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
body = ErrorResponse(
error={
"code": "VALIDATION_ERROR",
"message": "入力が正しくありません",
"detail": {"errors": exc.errors()},
}
)
return JSONResponse(status_code=422, content=body.model_dump())
これで、HTTPエラーもバリデーションエラーも同じ形で返せるようになります。クライアントは error.code を見て分岐し、UI表示は error.message を使う、といった設計がしやすくなります。
6. ページネーション設計:迷いがちな部分を「型」にする
一覧系APIは、実務ではほぼ必ずページネーションが必要になります。ここを適当にすると、後から互換性の問題が起きやすいです。
6.1 offset/limitの基本形
from pydantic import BaseModel
from typing import Generic, TypeVar
T = TypeVar("T")
class PageMeta(BaseModel):
limit: int
offset: int
total: int
class PageResponse(BaseModel, Generic[T]):
items: list[T]
meta: PageMeta
from fastapi import APIRouter, Query
from app.schemas import UserRead, PageResponse, PageMeta
router = APIRouter(prefix="/users", tags=["users"])
@router.get(
"",
summary="ユーザー一覧を取得",
response_model=PageResponse[UserRead],
)
def list_users(
limit: int = Query(20, ge=1, le=100, description="取得件数(最大100)"),
offset: int = Query(0, ge=0, description="開始位置"),
):
items = []
total = 0
return {"items": items, "meta": {"limit": limit, "offset": offset, "total": total}}
ポイントは、レスポンスに meta.total を含めておくことです。ページャーUIや「全件数」の表示に必要になります。
6.2 cursorベース(発展)
大量データやリアルタイム更新のある一覧では、offset が重くなったり、ページのズレが起きたりします。その場合は cursor ベース(next_cursor を返す)を検討します。
ここでは最小の形だけ示します。
from pydantic import BaseModel
class CursorPageMeta(BaseModel):
next_cursor: str | None = None
class CursorPageResponse(BaseModel):
items: list[UserRead]
meta: CursorPageMeta
APIの設計としては、どちらかに統一するのが運用上は楽です。混在させるなら、用途(管理画面はoffset、フィードはcursorなど)を明確にしておくのがおすすめです。
7. バージョニングと互換性:破壊的変更を「予告できる」設計へ
7.1 まずは /v1 を切る
前半で示したように、最初から /v1 を切っておくと、将来の破壊的変更がやりやすいです。
- 後方互換の変更(フィールド追加、説明追加など)は
/v1のまま - 破壊的変更(フィールド名変更、レスポンス形式変更など)は
/v2を作る
という整理ができます。
7.2 後方互換の基本ルール(運用で効きます)
互換性の事故は、だいたい「軽い気持ちの変更」から起きます。最低限、次のルールを意識すると安全です。
- 既存フィールドの削除はしない(非推奨→移行期間→削除の順)
- 既存フィールドの意味を変えない(同名で別の意味にしない)
- enum(文字列の取りうる値)の変更は慎重に(増やすのは比較的安全、減らすのは危険)
- エラー形式は一度決めたら壊さない(クライアントが依存しやすい)
FastAPIでは、deprecated=True を付けて、Swagger UI上でも「非推奨」を示せます。
@router.get(
"/legacy",
summary="旧エンドポイント(非推奨)",
deprecated=True,
)
def legacy():
...
この表示があるだけで、利用者への「移行してね」の合図になります。
8. OpenAPIの「出し方」も整える:docs_url・openapi_url・環境分離
本番では、ドキュメントを公開するかどうかを悩むことがあります。社内向けなら公開して良いですが、外部向けでは制限したい場合もあります。
import os
from fastapi import FastAPI
ENV = os.getenv("ENVIRONMENT", "dev")
app = FastAPI(
title="Example API",
version="1.0.0",
docs_url=None if ENV == "prod" else "/docs",
redoc_url=None if ENV == "prod" else "/redoc",
openapi_url=None if ENV == "prod" else "/openapi.json",
)
本番で完全に閉じるのが怖い場合は、社内IPだけ許可する、Basic認証をかける、管理者のみアクセスできるようにする、といった方法もあります。大切なのは「方針を決めて一貫させる」ことです。
9. チームで揃えたい「API設計の型」サンプル(実務で効く)
ここまでの話を、実務でよくある設計の型としてまとめます。
9.1 命名とURL設計(例)
- リソースは複数形:
/users,/articles - 取得:
GET /users/{id} - 一覧:
GET /users?limit=...&offset=... - 作成:
POST /users - 更新:
PUT /users/{id}(全体更新) orPATCH /users/{id}(部分更新) - 削除:
DELETE /users/{id}
9.2 レスポンスの揃え方(例)
- 一覧は
{"items": [...], "meta": {...}} - エラーは
{"error": {"code": "...", "message": "...", "detail": {...}}}
9.3 ドキュメント品質の最低ライン(例)
- すべてのエンドポイントに
summaryを付ける - 主要なものには
descriptionと例(example)を付ける - 主要なエラー(400/401/403/404/422/500)を
responsesに載せる - 非推奨は
deprecated=Trueで明示する
この「最低ライン」だけでも、Swagger UIの見やすさは見違えます。
10. 参考リンク(学びを深めたい方向け)
- FastAPI公式ドキュメント(OpenAPI / Docs)
- FastAPI – Bigger Applications(ルーター分割)
- FastAPI – Handling Errors(例外ハンドリング)
- OpenAPI Specification(仕様そのもの)
- Swagger UI(OpenAPIのUI)
まとめ(仕様書としてのSwagger UIを育てるコツ)
- FastAPIのOpenAPIは自動生成されますが、放っておくと「動作確認用」に留まりがちです。タグ・説明・例・エラー形式を整えることで、仕様書としての価値が上がります。
- とくに効くのは、共通エラーフォーマットとページネーションの型です。ここが揃うと、クライアント実装と運用がとても楽になります。
- バージョニングは、必要になってからだと移行コストが跳ね上がります。まずは
/v1を切り、互換性ルールを意識して変更を積み重ねるのがおすすめです。 - Swagger UIを「読む場所」にするために、summary・description・examplesを少しずつ足していきましょう。やればやるほど、未来の開発が優しくなります。
次回の記事としては、このOpenAPIをさらに活用して「クライアントSDK生成」や「契約テスト(contract testing)」に進む流れも相性が良いです。必要なら、そのテーマで続編もまとめますね。
