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

このままだと HTTPExceptiondetail がそのまま出るので、実務では「例外ハンドラ」で統一フォーマットに整形するのがおすすめです。

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)」に進む流れも相性が良いです。必要なら、そのテーマで続編もまとめますね。


投稿者 greeden

コメントを残す

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

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