API Documentation with Swagger — Deep Dive

Customizing Swagger UI in FastAPI

FastAPI’s default Swagger UI works out of the box, but production APIs need customization:

from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi

app = FastAPI(
    docs_url="/api/docs",
    redoc_url="/api/redoc",
    openapi_url="/api/openapi.json",
)

def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema

    openapi_schema = get_openapi(
        title="Acme User Service",
        version="2.1.0",
        description="""
## Overview

The User Service manages user accounts, authentication, and profiles.

### Authentication

All endpoints require a Bearer token in the Authorization header.
Get a token via `POST /auth/token`.

### Rate Limits

- Standard: 100 requests/minute
- Authenticated: 1000 requests/minute
- Burst: 50 requests/second

### Environments

| Environment | Base URL |
|---|---|
| Production | `https://api.acme.com` |
| Staging | `https://api-staging.acme.com` |
| Local | `http://localhost:8000` |
        """,
        routes=app.routes,
    )

    # Add servers
    openapi_schema["servers"] = [
        {"url": "https://api.acme.com", "description": "Production"},
        {"url": "https://api-staging.acme.com", "description": "Staging"},
        {"url": "http://localhost:8000", "description": "Local development"},
    ]

    # Add external docs link
    openapi_schema["externalDocs"] = {
        "description": "Full developer guide",
        "url": "https://docs.acme.com",
    }

    app.openapi_schema = openapi_schema
    return app.openapi_schema

app.openapi = custom_openapi

The description field supports full Markdown, which Swagger UI and ReDoc render beautifully. Use it for authentication guides, rate limit tables, and environment information.

Swagger UI theming

Override Swagger UI’s appearance by serving a custom configuration:

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI(
    swagger_ui_parameters={
        "deepLinking": True,
        "displayRequestDuration": True,
        "filter": True,
        "showExtensions": True,
        "showCommonExtensions": True,
        "syntaxHighlight.theme": "monokai",
        "tryItOutEnabled": True,
        "persistAuthorization": True,
        "defaultModelsExpandDepth": 3,
        "defaultModelExpandDepth": 3,
        "docExpansion": "list",
    }
)

Key parameters:

  • displayRequestDuration — shows how long each test request took
  • filter — adds a search box for filtering endpoints
  • persistAuthorization — keeps the auth token after page refresh
  • tryItOutEnabled — all endpoints start in “try it out” mode
  • docExpansion"list" shows operations collapsed, "full" shows them expanded

For full visual customization, serve Swagger UI from your own static files:

from fastapi.openapi.docs import get_swagger_ui_html

@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui():
    return get_swagger_ui_html(
        openapi_url="/api/openapi.json",
        title="Acme API Docs",
        swagger_css_url="/static/swagger-custom.css",
        swagger_favicon_url="/static/favicon.png",
    )

Structured endpoint documentation

Rich endpoint documentation with multiple response examples:

from fastapi import FastAPI, Path, Query, HTTPException
from pydantic import BaseModel, Field

class UserResponse(BaseModel):
    id: int = Field(..., examples=[42])
    name: str = Field(..., examples=["Alice Chen"])
    email: str = Field(..., examples=["alice@example.com"])
    role: str = Field(..., examples=["admin"])

class ErrorResponse(BaseModel):
    error: str = Field(..., examples=["USER_NOT_FOUND"])
    message: str = Field(..., examples=["No user with ID 999 exists"])
    request_id: str = Field(..., examples=["req_abc123"])

@app.get(
    "/users/{user_id}",
    response_model=UserResponse,
    responses={
        200: {
            "description": "User found",
            "content": {
                "application/json": {
                    "examples": {
                        "admin_user": {
                            "summary": "Admin user",
                            "value": {
                                "id": 1,
                                "name": "Admin User",
                                "email": "admin@acme.com",
                                "role": "admin",
                            },
                        },
                        "regular_user": {
                            "summary": "Regular user",
                            "value": {
                                "id": 42,
                                "name": "Alice Chen",
                                "email": "alice@example.com",
                                "role": "member",
                            },
                        },
                    }
                }
            },
        },
        404: {
            "model": ErrorResponse,
            "description": "User not found",
        },
        401: {
            "model": ErrorResponse,
            "description": "Missing or invalid authentication token",
        },
    },
    tags=["Users"],
    summary="Get user by ID",
    description="Retrieves a single user account. Requires `read:users` scope.",
)
async def get_user(
    user_id: int = Path(..., ge=1, description="User's unique identifier"),
    include_metadata: bool = Query(
        False,
        description="Include account metadata (creation date, last login)",
    ),
):
    """
    Fetch a user's profile information.

    The response includes basic profile data. Set `include_metadata=true`
    to include account timestamps and login history.

    **Required scope:** `read:users`
    """
    ...

Multiple examples in the 200 response create a dropdown in Swagger UI, letting developers see different response shapes without making requests.

Flask API documentation with flask-smorest

from flask import Flask
from flask_smorest import Api, Blueprint
from marshmallow import Schema, fields as ma_fields

app = Flask(__name__)
app.config.update({
    "API_TITLE": "Acme API",
    "API_VERSION": "v1",
    "OPENAPI_VERSION": "3.0.3",
    "OPENAPI_URL_PREFIX": "/api",
    "OPENAPI_SWAGGER_UI_PATH": "/docs",
    "OPENAPI_SWAGGER_UI_URL": "https://cdn.jsdelivr.net/npm/swagger-ui-dist/",
    "OPENAPI_REDOC_PATH": "/redoc",
    "OPENAPI_REDOC_URL": "https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js",
})

api = Api(app)

class UserSchema(Schema):
    id = ma_fields.Int(dump_only=True, metadata={"description": "Unique ID"})
    name = ma_fields.Str(required=True, metadata={"description": "Full name"})
    email = ma_fields.Email(required=True)

class UserQuerySchema(Schema):
    search = ma_fields.Str(metadata={"description": "Search by name or email"})
    is_active = ma_fields.Bool(load_default=True)

users_blp = Blueprint("users", __name__, url_prefix="/users", description="User operations")

@users_blp.route("/")
@users_blp.arguments(UserQuerySchema, location="query")
@users_blp.response(200, UserSchema(many=True))
@users_blp.doc(summary="List users", description="Returns paginated list of users")
def list_users(args):
    """List all users matching the query."""
    ...

api.register_blueprint(users_blp)

flask-smorest generates clean OpenAPI specs from marshmallow schemas and provides both Swagger UI and ReDoc.

Django REST Framework with drf-spectacular

# views.py
from drf_spectacular.utils import extend_schema, OpenApiExample, OpenApiParameter
from rest_framework import viewsets, serializers

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["id", "name", "email", "role", "created_at"]

class UserViewSet(viewsets.ModelViewSet):
    serializer_class = UserSerializer

    @extend_schema(
        summary="List users",
        description="Returns paginated list of users. Supports filtering by role.",
        parameters=[
            OpenApiParameter(
                name="role",
                type=str,
                enum=["admin", "member", "viewer"],
                description="Filter by user role",
            ),
        ],
        examples=[
            OpenApiExample(
                "Success",
                value={
                    "count": 2,
                    "results": [
                        {"id": 1, "name": "Alice", "email": "alice@acme.com", "role": "admin"},
                    ],
                },
                response_only=True,
            ),
        ],
    )
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

Auto-generating code samples

Add code samples to your documentation with a custom extension:

def add_code_samples(openapi_schema: dict) -> dict:
    """Inject x-codeSamples for ReDoc rendering."""
    for path, methods in openapi_schema.get("paths", {}).items():
        for method, details in methods.items():
            if method in ("get", "post", "put", "patch", "delete"):
                url = f"https://api.acme.com{path}"

                samples = [
                    {
                        "lang": "curl",
                        "label": "cURL",
                        "source": f'curl -X {method.upper()} "{url}" \\\n'
                                  f'  -H "Authorization: Bearer $TOKEN" \\\n'
                                  f'  -H "Content-Type: application/json"',
                    },
                    {
                        "lang": "python",
                        "label": "Python",
                        "source": f'import httpx\n\n'
                                  f'response = httpx.{method}(\n'
                                  f'    "{url}",\n'
                                  f'    headers={{"Authorization": "Bearer TOKEN"}},\n'
                                  f')\n'
                                  f'print(response.json())',
                    },
                    {
                        "lang": "javascript",
                        "label": "JavaScript",
                        "source": f'const response = await fetch("{url}", {{\n'
                                  f'  method: "{method.upper()}",\n'
                                  f'  headers: {{ "Authorization": "Bearer TOKEN" }},\n'
                                  f'}});\n'
                                  f'const data = await response.json();',
                    },
                ]

                details["x-codeSamples"] = samples

    return openapi_schema

ReDoc renders x-codeSamples as tabbed code blocks in the right panel. Swagger UI doesn’t support this extension natively but plugins exist.

CI integration for documentation quality

Validate documentation completeness in your CI pipeline:

# scripts/check_docs_quality.py
import json
import sys

with open("openapi.json") as f:
    spec = json.load(f)

issues = []

for path, methods in spec.get("paths", {}).items():
    for method, details in methods.items():
        if method in ("get", "post", "put", "patch", "delete"):
            endpoint = f"{method.upper()} {path}"

            if not details.get("summary"):
                issues.append(f"{endpoint}: missing summary")

            if not details.get("description"):
                issues.append(f"{endpoint}: missing description")

            if not details.get("tags"):
                issues.append(f"{endpoint}: missing tags")

            responses = details.get("responses", {})
            if "400" not in responses and method in ("post", "put", "patch"):
                issues.append(f"{endpoint}: missing 400 error response")

            if "401" not in responses:
                issues.append(f"{endpoint}: missing 401 error response")

for name, schema in spec.get("components", {}).get("schemas", {}).items():
    props = schema.get("properties", {})
    for prop_name, prop in props.items():
        if not prop.get("description") and not prop.get("$ref"):
            issues.append(f"Schema {name}.{prop_name}: missing description")

if issues:
    print(f"❌ Found {len(issues)} documentation issues:")
    for issue in issues:
        print(f"  - {issue}")
    sys.exit(1)
else:
    print("✅ Documentation quality check passed")

Run this in CI alongside your tests. Treat missing descriptions and undocumented errors as build failures — documentation quality degrades quickly without enforcement.

Hosting documentation separately

For public APIs, host docs independently from the API server:

# Generate static HTML from OpenAPI spec using Redocly CLI
npx @redocly/cli build-docs openapi.json -o docs/index.html

# Or generate a full docs site with Mintlify/ReadMe/Stoplight

Static docs can be deployed to any CDN, load instantly, and don’t depend on the API being online. Many teams auto-deploy docs on every merge to main.

One thing to remember: Swagger UI is the starting point — production-quality API documentation needs custom examples, error documentation, code samples, and CI checks to ensure every endpoint is described well enough that a new developer can integrate without asking questions.

pythonwebapisswaggerdocumentationfastapidjango

See Also

  • Python Aiohttp Client Understand Aiohttp Client through a practical analogy so your Python decisions become faster and clearer.
  • Python Api Client Design Why building your own API client in Python is like creating a TV remote that only has the buttons you actually need.
  • Python Api Mocking Responses Why testing with fake API responses is like rehearsing a play with stand-ins before the real actors show up.
  • Python Api Pagination Clients Why APIs send data in pages, and how Python handles it — like reading a book one chapter at a time instead of swallowing the whole thing.
  • Python Beautifulsoup Understand Beautifulsoup through a practical analogy so your Python decisions become faster and clearer.