OpenAPI and the generated docs
The schema is generated from your type hints, which is FastAPI’s headline feature and also why a sloppy signature produces a sloppy public contract. What you tune is mostly naming, examples and what you hide.
What ends up in the schema
@app.post(
"/orders",
response_model=OrderOut,
status_code=201,
summary="Create an order",
tags=["orders"],
responses={409: {"model": ErrorOut, "description": "Duplicate"}},
)
async def create(body: OrderIn) -> OrderOut:
"""Longer prose here becomes the endpoint description."""The docstring becomes the description, summary the one-line title, and
responses documents the failures — which the schema otherwise claims do not
exist. An API that documents only its 200 is an API whose clients handle only
its 200.
tags is what groups endpoints in the docs page. Without them everything lands
in “default” and a fifty-endpoint API becomes unreadable.
Operation ids decide the generated client’s method names
def unique_id(route: APIRoute) -> str:
return f"{route.tags[0]}_{route.name}"
app = FastAPI(generate_unique_id_function=unique_id)The default operation id is long and includes the path, so a generated
TypeScript client ends up with createOrdersOrdersPost(). Setting this gives
you orders_create(). It costs three lines and it is the difference between a
client SDK people use and one they wrap.
Gotcha: operation ids are part of your public contract once anyone generates a client from them. Changing the scheme later renames every method in every consumer.
Examples are worth more than descriptions
class OrderIn(BaseModel):
sku: str
qty: int
model_config = ConfigDict(
json_schema_extra={
"examples": [{"sku": "ABC-123", "qty": 2}],
},
)A reader copies the example and changes the values. A prose description of each field gets skimmed. If you do one thing to the schema, do this.
Hiding what should not be public
@app.get("/internal/flush", include_in_schema=False)
async def flush(): ...include_in_schema=False removes an endpoint from the docs — it does not
secure it. The route still exists and still answers. Undocumented is not
private; that needs auth.
For the docs pages themselves in production:
app = FastAPI(
docs_url=None if settings.env == "prod" else "/docs",
redoc_url=None,
openapi_url=None if settings.env == "prod" else "/openapi.json",
)Turning off docs_url while leaving openapi_url on hides the page and serves
the schema, which is the mistake — the schema is the thing worth having, and it
is the thing you meant to hide.
Versioning
Two live approaches, and the choice shows up in the schema either way:
| Approach | Looks like |
|---|---|
| Path | /v1/orders, a router prefix per version |
| Header | Accept: application/vnd.api.v2+json |
Path versioning with one APIRouter per version is what most teams ship,
because it is visible in logs, cacheable, and trivial to route at a proxy. See
REST Versioning and Pagination.
v1 = APIRouter(prefix="/v1", tags=["v1"])
app.include_router(v1)Related
Interview angle 6
- “How does FastAPI generate OpenAPI?” - from the type hints and Pydantic models on each route, plus
summary,tags,responsesand the docstring. The schema is only as good as the signature, which is the trade for getting it free. - “Why set
generate_unique_id_function?” - the default operation id includes the path, so a generated client getscreateOrdersOrdersPost(). A custom function givesorders_create(). It becomes public contract once anyone generates a client, so pick the scheme early. - “How do you document failures?” - the
responsesargument, with a model per status. Otherwise the schema asserts that only 200 exists, and clients are written to match. - “Does
include_in_schema=Falsesecure an endpoint?” - no. It hides it from the docs; the route still answers. Undocumented is not private — that needs authentication. - “How do you disable docs in production?” - set
docs_urlandopenapi_urltoNone. Turning off onlydocs_urlstill serves/openapi.json, which is the part actually worth hiding. - “Path or header versioning?” - path, for most teams: visible in logs, cacheable, routable at a proxy, and one
APIRouterper version. Header versioning is cleaner in theory and harder to operate.