Skip to main content

OpenAPI Customization

FastAPI generates OpenAPI 3.1 specs automatically. Customizing that spec — adding metadata, examples, tag groupings, and hiding internal endpoints — makes your API documentation professional and usable.

Learning Focus

By the end of this lesson you can: set API metadata, organize routes by tag, add request/response examples, hide internal endpoints, and generate a client SDK from the OpenAPI spec.

API Metadata

app/main.py
from fastapi import FastAPI

app = FastAPI(
title="My Production API",
version="2.1.0",
description="""
## Overview

This API provides access to the product catalog, user management, and order processing.

## Authentication

All endpoints except `/health` and `/auth/token` require a valid JWT bearer token.

## Rate Limits

- Public endpoints: 20 requests/minute
- Authenticated endpoints: 200 requests/minute
""",
terms_of_service="https://example.com/terms",
contact={
"name": "API Support",
"url": "https://example.com/support",
"email": "api@example.com",
},
license_info={
"name": "MIT",
"url": "https://opensource.org/licenses/MIT",
},
openapi_url="/api/v1/openapi.json", # Custom URL
docs_url="/api/v1/docs",
redoc_url="/api/v1/redoc",
)

Organizing by Tags

app/main.py
app = FastAPI(
openapi_tags=[
{
"name": "auth",
"description": "Authentication and token management",
},
{
"name": "products",
"description": "Product catalog — browse, search, and manage items",
"externalDocs": {
"description": "Product schema docs",
"url": "https://docs.example.com/products",
},
},
{
"name": "admin",
"description": "Administrative operations. **Requires admin role.**",
},
]
)

Adding Route Examples

app/routers/items.py
from fastapi import APIRouter
from pydantic import BaseModel, Field

class ItemCreate(BaseModel):
name: str = Field(..., examples=["Premium Widget"])
price: float = Field(..., examples=[29.99])
description: str | None = Field(None, examples=["A high-quality widget"])

@router.post(
"/",
response_model=ItemResponse,
status_code=201,
summary="Create an item",
description="Creates a new item in the catalog. Returns the full item object with generated ID.",
response_description="The created item",
openapi_extra={
"requestBody": {
"content": {
"application/json": {
"examples": {
"basic": {"value": {"name": "Widget", "price": 9.99}},
"full": {"value": {"name": "Premium Widget", "price": 49.99, "description": "High quality"}},
}
}
}
}
},
)
async def create_item(item: ItemCreate) -> ItemResponse:
...

Hiding Internal Routes

app/routers/internal.py
from fastapi import APIRouter

# Entire router hidden from docs
router = APIRouter(include_in_schema=False)

@router.get("/internal/metrics")
async def metrics() -> dict:
return {"memory_mb": 150, "requests": 10000}

Or hide individual routes:

app/main.py
@app.get("/health", include_in_schema=False)
async def health() -> dict:
return {"status": "ok"}

Restricting Docs Access

app/main.py
from fastapi import FastAPI
import os

# Only expose docs in non-production
docs_url = "/docs" if os.getenv("ENV") != "production" else None
redoc_url = "/redoc" if os.getenv("ENV") != "production" else None

app = FastAPI(docs_url=docs_url, redoc_url=redoc_url)

Generating Client SDKs

generate-sdk.sh
# Install openapi-generator
npm install @openapitools/openapi-generator-cli -g

# Generate TypeScript client
openapi-generator-cli generate \
-i http://localhost:8000/openapi.json \
-g typescript-axios \
-o ./clients/typescript

# Generate Python client
openapi-generator-cli generate \
-i http://localhost:8000/openapi.json \
-g python \
-o ./clients/python

Common Pitfalls

PitfallCause / SymptomFix
Docs emptyAll routes have include_in_schema=FalseRemove flag from non-internal routes
Tag not appearingTag not in openapi_tags listAdd tag definition to FastAPI(openapi_tags=[...])
Examples not showingUsing Pydantic v1 schema_extraUse Pydantic v2 json_schema_extra in model_config
operation_id collisionTwo routes auto-generate same IDSet operation_id= explicitly on each route
Docs blocked in productionAll routes hiddenUse environment variable to conditionally set docs_url

What's Next