サイトアイコン IT & ライフハックブログ|学びと実践のためのアイデア集

FastAPIのOpenAPI活用術:Swagger UIを「仕様書」として育てるAPI設計(バージョニング・エラー設計・ページネーションまで)

green snake

Photo by Pixabay on Pexels.com

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}(全体更新) or PATCH /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. 参考リンク(学びを深めたい方向け)


まとめ(仕様書としてのSwagger UIを育てるコツ)

  • FastAPIのOpenAPIは自動生成されますが、放っておくと「動作確認用」に留まりがちです。タグ・説明・例・エラー形式を整えることで、仕様書としての価値が上がります。
  • とくに効くのは、共通エラーフォーマットとページネーションの型です。ここが揃うと、クライアント実装と運用がとても楽になります。
  • バージョニングは、必要になってからだと移行コストが跳ね上がります。まずは /v1 を切り、互換性ルールを意識して変更を積み重ねるのがおすすめです。
  • Swagger UIを「読む場所」にするために、summary・description・examplesを少しずつ足していきましょう。やればやるほど、未来の開発が優しくなります。

次回の記事としては、このOpenAPIをさらに活用して「クライアントSDK生成」や「契約テスト(contract testing)」に進む流れも相性が良いです。必要なら、そのテーマで続編もまとめますね。


モバイルバージョンを終了