green snake
Photo by Pixabay on Pexels.com

Building Useful API Docs: Practical FastAPI × OpenAPI Design — Schema Design, Error Models, Examples & Exceptions, Tagging, Custom UI, Client Auto-Generation


Summary (The Big Picture)

  • Make OpenAPI the center of your specification, and use FastAPI’s response_model, responses, examples, and schemas to create readable, consistent API documentation.
  • Standardize exceptions and error responses with a shared schema, and consistently format them using HTTPException and exception handlers.
  • Organize tags, versions, server info, and security definitions so consumers don’t get lost.
  • Auto-generate client SDKs from the finalized OpenAPI to keep quality aligned with implementation.
  • Enhance your docs with explanations, visuals, multilingual notes, and rich Examples so backend, QA, and frontend share the same understanding.

Who Will Benefit

  • Student Engineer A: Built an API with FastAPI, but the docs are poor and users ask many questions. Wants to learn how to standardize examples and error models.
  • Small Team B: Providing an external public API for a client project. Wants to reduce support costs via OpenAPI hygiene and SDK auto-generation.
  • SaaS Team C: Ships features quickly. Wants to link schemas, tests, and docs, and make breaking-change management explicit.

1) First Decision: Make OpenAPI the Source of Design Truth

OpenAPI (formerly Swagger) is a machine- and human-readable spec for HTTP APIs. FastAPI generates OpenAPI automatically from type hints and Pydantic (v2). The more correct your schemas, the higher your documentation quality.

Decision points:

  • Always model inputs (Body/Query/Path/Headers) and outputs (response_model).
  • Model failure cases too, and ensure code always returns the same shape.
  • Enrich with Examples so consumers can try requests easily.

2) Schema Fundamentals: response_model and Examples

2.1 Separate Input and Output Models

# schemas/article.py
from pydantic import BaseModel, Field
from datetime import datetime

class ArticleCreate(BaseModel):
    title: str = Field(..., max_length=120, description="Article title")
    body: str = Field(..., description="Body (Markdown allowed)")

class Article(BaseModel):
    id: int
    title: str
    body: str
    created_at: datetime
    class Config:
        from_attributes = True

2.2 Apply at Endpoints with Examples

# 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": "Created successfully",
            "content": {
                "application/json": {
                    "examples": {
                        "ok": {
                            "summary": "Happy-path example",
                            "value": {
                                "id": 101,
                                "title": "Principles of API Design",
                                "body": "Design starts by working backward from the spec…",
                                "created_at": "2025-10-02T09:00:00Z"
                            }
                        }
                    }
                }
            },
        }
    },
)
async def create_article(payload: ArticleCreate):
    # Implementation omitted (DB save, etc.)
    return Article(id=101, **payload.dict(), created_at="2025-10-02T09:00:00Z")

Key points:

  • You can put examples directly in responses.
  • response_model keeps auto-docs and schemas in sync.

3) Unify Failures: Error Model & Exception Handlers

3.1 Common Error Schema

# schemas/error.py
from pydantic import BaseModel, Field

class ErrorDetail(BaseModel):
    code: str = Field(..., description="Machine-readable error code")
    message: str = Field(..., description="Human-readable explanation")
    info: dict | None = Field(None, description="Optional extra context")

class ErrorResponse(BaseModel):
    detail: ErrorDetail

3.2 Normalize with a Handler

# 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 Document Error Examples

# routers/articles.py (excerpt)
from schemas.error import ErrorResponse

@router.get(
    "/{article_id}",
    response_model=Article,
    responses={
        404: {
            "model": ErrorResponse,
            "description": "Not found",
            "content": {
                "application/json": {
                    "examples": {
                        "not_found": {
                            "summary": "Unknown ID",
                            "value": {"detail": {"code": "ARTICLE_NOT_FOUND", "message": "Article not found"}}
                        }
                    }
                }
            },
        }
    },
)
async def get_article(article_id: int):
    # Implementation omitted
    raise DomainError("ARTICLE_NOT_FOUND", "Article not found")

Decision points:

  • Format exceptions in one place, keep the shape identical everywhere.
  • Provide failure examples in responses to align expectations.

4) Organize Tags, Descriptions, and Meta

4.1 Tags with Descriptions

app = FastAPI(
    title="OpenAPI Oriented API",
    description="An API centered on its specification. It has three domains: articles, users, and auth.",
    version="1.2.0",  # Spec version
    openapi_tags=[
        {"name": "articles", "description": "Create / read / update / delete articles"},
        {"name": "users", "description": "User management"},
        {"name": "auth", "description": "Authentication & tokens"},
    ],
)

4.2 Servers, Contact, License

app.openapi_servers = [
    {"url": "https://api.example.com", "description": "Production"},
    {"url": "https://stg-api.example.com", "description": "Staging"},
]
app.openapi_contact = {"name": "API Support", "email": "support@example.com"}
app.openapi_license_info = {"name": "MIT"}

Decision points:

  • Split tags by functional domain with short descriptions.
  • Declare servers and contacts to reduce user confusion.

5) Security Definitions and Usage

5.1 Security Scheme (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")
    # Verification omitted
    return {"sub": "alice"}

# Router side
@router.get("/me", tags=["auth"])
async def me(user = Depends(get_current_user)):
    return user

5.2 Reflection in OpenAPI

Using HTTPBearer (etc.) makes FastAPI auto-generate securitySchemes. The presence of dependencies clearly indicates which endpoints require auth.

Decision points:

  • Add the dependency wherever auth is required so it’s mandatory in the spec too.
  • If you have scopes, use an OAuth2 scheme.

6) Reusing responses

As responses grow, you may repeat the same definitions. Refactor into functions/dicts for consistency.

from schemas.error import ErrorResponse

def error_responses(resource: str):
    return {
        400: {"model": ErrorResponse, "description": "Validation error"},
        404: {
            "model": ErrorResponse,
            "description": f"{resource} not found",
        },
        409: {"model": ErrorResponse, "description": "Conflict or duplicate"},
    }

@router.put(
    "/{article_id}",
    response_model=Article,
    responses=error_responses("Article")
)
async def update_article(article_id: int, payload: ArticleCreate):
    ...

Decision points:

  • Unify vocabulary (same phenomena → same code and phrasing) to reduce learning cost.
  • Don’t document responses you never return. Keep the spec honest.

7) Add More Examples to Boost Usability

  • Success examples: one each for minimal, representative, and boundary (empty element / max length).
  • Failure examples: typical validation error, auth error, conflict.
  • Narration: use summary and short description for context.

Tips:

  • Large examples can live in separate JSON files and be loaded for reuse.
  • The easier “Try it out” succeeds in Swagger UI, the fewer support tickets you’ll get.

8) Export OpenAPI and Generate Client SDKs

8.1 Export

# Generate (you can also do this without booting the app if you construct the app in code)
python -c "import json; \
from app.main import app; \
print(json.dumps(app.openapi(), ensure_ascii=False))" > openapi.json

Or fetch /openapi.json after boot.

8.2 SDK Auto-Generation (e.g., OpenAPI Generator)

# TypeScript Axios client
openapi-generator generate \
  -i openapi.json \
  -g typescript-axios \
  -o client-ts

Many targets exist (python, kotlin, swift, go, …). You’ll get type-safe calls, and can track spec changes as diffs.

Decision points:

  • Automate generation in CI, surfacing breaking changes in PRs.
  • Publish generated clients in a separate repository for easy distribution.

9) Versioning and Handling Breaking Changes

  • Path versioning: /api/v1 → later run /api/v2 in parallel. Provide a deprecation window for the old version.
  • Schema compatibility: removing fields or changing types is breaking; adding new fields is generally backward compatible.
  • Change logs: Use the same wording across OpenAPI, implementation, and SDK release notes for traceability.

Decision points:

  • Isolate breaking changes behind a new versioned path.
  • Include a migration guide in the docs and state deadlines.

10) Strengthen Look & Meta of Docs

  • Keep title, description, and tags concise, with an introductory paragraph for first-timers.
  • Gather critical constraints at the top (rate limits, size limits, time zones, datetime formats).
  • Offer both /docs (Swagger UI) and /redoc. ReDoc shines for long-form reading.

11) Sample: Minimal End-to-End API Skeleton

# main.py
from fastapi import FastAPI
from routers.articles import router as article_router

app = FastAPI(
    title="OpenAPI Oriented API",
    description="Sample designed around the spec",
    version="1.2.0",
    openapi_tags=[
        {"name": "articles", "description": "Article features"},
        {"name": "auth", "description": "Authentication"},
    ],
)

app.include_router(article_router, prefix="/api/v1")

@app.get("/health", tags=["meta"])
def health():
    return {"ok": True}

Readable specs directly improve implementation quality and support costs. Start small—standardize examples and error models first.


12) Detect Schema Breakage Early with Tests

  • In CI, generate → validate the OpenAPI JSON (use linters/tools).
  • Add snapshot tests for key endpoints (catch diffs in responses and examples).
  • Continuously verify that the shape of validation errors, types, and required fields has not drifted.

13) Common Pitfalls and Remedies

Symptom Cause Remedy
Impl & docs drift Hand-written docs Prefer auto-generated docs; keep responses and examples in code
Inconsistent failure shapes Returning raw exceptions Use a shared error model + handler
Usage unclear Missing examples Provide Examples for each response, including edge cases
Auth unclear No scheme defined Add dependencies that emit securitySchemes; mark required scope
Chaos from breaking changes No version isolation Split under /api/v2, add migration guide + deadlines

14) Adoption Roadmap

  1. Introduce response_model and a shared error model (lock down success/failure shapes).
  2. Add Examples to major endpoints so Swagger UI just works.
  3. Organize tags, descriptions, and server info; put intro & constraint list at the top.
  4. Wire OpenAPI export and SDK generation into CI.
  5. Route breaking changes via a new version, migrate in phases.

References


Takeaways

  • Model inputs/outputs and failure cases; unify exceptions with a shared error model.
  • Use responses and Examples to make the spec try-friendly. Organize tags, descriptions, security, and servers.
  • Export OpenAPI and auto-generate client SDKs so spec and implementation run on the same rails.
  • Handle breaking changes with versioned paths and a migration guide. Start today—build documentation people can actually use.

By greeden

Leave a Reply

Your email address will not be published. Required fields are marked *

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