Backend / Web frameworks / FastAPI / 18_openapi_and_docs.md

OpenAPI and the generated docs

Updated 6 interview angles 3 min read source
On this page7
  1. What ends up in the schema
  2. Operation ids decide the generated client’s method names
  3. Examples are worth more than descriptions
  4. Hiding what should not be public
  5. Versioning
  6. Related
  7. Interview angle

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

python
@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

python
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

python
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

python
@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:

python
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.

python
v1 = APIRouter(prefix="/v1", tags=["v1"])
app.include_router(v1)

Interview angle 6

  • “How does FastAPI generate OpenAPI?” - from the type hints and Pydantic models on each route, plus summary, tags, responses and 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 gets createOrdersOrdersPost(). A custom function gives orders_create(). It becomes public contract once anyone generates a client, so pick the scheme early.
  • “How do you document failures?” - the responses argument, with a model per status. Otherwise the schema asserts that only 200 exists, and clients are written to match.
  • “Does include_in_schema=False secure 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_url and openapi_url to None. Turning off only docs_url still 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 APIRouter per version. Header versioning is cleaner in theory and harder to operate.