Overview

FastAPI generates an OpenAPI 3.1 schema from your route declarations and Pydantic models. The schema powers /docs (Swagger UI), /redoc (ReDoc), and any client codegen pipeline. The schema is only as accurate as your annotations; leaving response_model unset or status_code at its default produces a schema that misleads API consumers and breaks generated clients. This page covers what to set on every route and how to use the generated schema downstream. See fastapi-pydantic for the model side of the contract.

Set response_model, status_code, tags, and summary on every route

Treat these four parameters as mandatory. Omitting any of them makes the schema incomplete.

from fastapi import APIRouter
from app.models import OrderCreate, OrderRead
 
router = APIRouter(prefix="/orders", tags=["orders"])
 
@router.post(
    "/",
    response_model=OrderRead,
    status_code=201,
    summary="Place a new order",
    response_description="The created order",
)
async def create_order(payload: OrderCreate, db=Depends(get_db)) -> OrderRead:
    """
    Place a new order for the authenticated customer.
 
    - Validates inventory before confirming.
    - Returns the order with its assigned `order_id`.
    """
    ...
  • response_model filters the return value through Pydantic. Extra internal fields do not leak to clients. FastAPI also generates the response schema from this model.
  • status_code defaults to 200. POST should be 201, DELETE with no body should be 204. See http-status-codes.
  • tags group routes in Swagger UI. Use one tag per logical resource, matching the router’s resource name.
  • summary is the one-line label shown collapsed in the docs. The route docstring becomes the expanded description.

Declare all error responses with responses

FastAPI only adds the success response to the schema by default. Add error shapes with the responses parameter so clients know what to expect on failure.

@router.get(
    "/{order_id}",
    response_model=OrderRead,
    responses={
        404: {"description": "Order not found"},
        403: {"description": "Insufficient permissions"},
    },
)
async def get_order(order_id: str, user=Depends(get_current_user)) -> OrderRead: ...

Provide model inside the response dict to generate a schema for the error body:

from app.models import ErrorDetail
 
responses={404: {"model": ErrorDetail, "description": "Order not found"}}

Use APIRouter to apply shared metadata at the router level

Setting prefix, tags, and dependencies on the router avoids repeating them on every route. The include_router call in main.py composes routers without duplicating configuration.

# routers/orders.py
router = APIRouter(prefix="/orders", tags=["orders"], dependencies=[Depends(verify_api_key)])
 
# main.py
app.include_router(orders.router)
app.include_router(users.router)

Router-level dependencies run for every route in that router, which is the correct place for authentication guards. See fastapi-dependencies for the dependency injection pattern.

Suppress internal routes from the schema when needed

Development-only routes, health checks, and internal admin endpoints should not appear in the public schema. Set include_in_schema=False to hide them.

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

For separate public and internal schemas, create two FastAPI instances with openapi_url set differently, or use middleware to filter routes by tag before generating the schema.

Choose ReDoc for documentation, Swagger UI for exploration

Both are served by default. /redoc renders a clean two-panel view suited for sharing with API consumers. /docs (Swagger UI) has a “Try it out” button suited for developer exploration.

app = FastAPI(
    title="Orders API",
    version="1.0.0",
    docs_url="/docs",
    redoc_url="/redoc",
    openapi_url="/openapi.json",
)

In production, consider disabling /docs and /redoc entirely to reduce the attack surface, and expose the schema only to authenticated tooling. Set docs_url=None, redoc_url=None to suppress both.

Generate typed clients from the OpenAPI schema

The schema at /openapi.json is the input for client codegen. openapi-typescript-codegen and openapi-generator both consume it.

# Generate a TypeScript client
npx openapi-typescript-codegen \
  --input http://localhost:8000/openapi.json \
  --output ./src/api \
  --client axios
 
# Generate a Python client
openapi-generator generate \
  -i http://localhost:8000/openapi.json \
  -g python \
  -o ./clients/python

Generated clients stay in sync with the API as long as response_model and request body types are accurate. A route that returns Any or omits response_model produces an untyped client method. See fastapi-pydantic for keeping models strict enough to generate useful clients.