Skip to content

OpenAPI & Docs Interview Questions & Answers

13 questions Updated 2026-06-20 Share:

FastAPI OpenAPI and docs interview questions — Swagger UI, ReDoc, schema customisation, security schemes, tags, versioning and disabling docs in production.

13 of 13

At application startup FastAPI inspects every registered route — its path, HTTP method, type annotations, Pydantic models, Query/Body/Header definitions and docstrings — and assembles an OpenAPI 3.x JSON document.

app = FastAPI(title="My API", version="1.0.0")

@app.get("/items/{id}", summary="Fetch an item")
async def get_item(id: int) -> Item:
    """Return a single item by its numeric ID."""
    ...
# OpenAPI schema available at /openapi.json

The schema drives:

  • /docs — Swagger UI (interactive browser)
  • /redoc — ReDoc (readable reference)
  • Client code generators (orval, openapi-generator)

Rule of thumb: treat the auto-generated schema as the source of truth for your API contract; add summary, description, responses and tags to keep it accurate.

Both render the same OpenAPI schema, but for different audiences:

Swagger UI ReDoc
URL /docs /redoc
Primary use Interactive testing (try-it-out) Readable documentation
Layout Split-pane, request builder Three-panel, prose-first
Authentication OAuth2/Bearer flow built-in Read-only
app = FastAPI(
    docs_url="/docs",       # default
    redoc_url="/redoc",     # default
)

Both can be moved or disabled:

app = FastAPI(docs_url=None, redoc_url="/api-docs")

Rule of thumb: expose both; use Swagger UI for development/testing, share ReDoc links for external consumer documentation.

Set docs_url=None and redoc_url=None (and optionally openapi_url=None) in the FastAPI() constructor. Typically done via an environment flag:

import os
from fastapi import FastAPI

DEBUG = os.getenv("DEBUG", "false").lower() == "true"

app = FastAPI(
    docs_url="/docs" if DEBUG else None,
    redoc_url="/redoc" if DEBUG else None,
    openapi_url="/openapi.json" if DEBUG else None,
)

Setting openapi_url=None prevents schema scraping even if someone guesses the /docs URL, since the UI needs the schema to render.

Rule of thumb: disable the schema endpoint (openapi_url=None) in production — leaking your API structure is a security surface even if no credentials are exposed.

Override app.openapi() to intercept and modify the schema dict before it's served:

from fastapi.openapi.utils import get_openapi

def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema
    schema = get_openapi(
        title="My API",
        version="2.0.0",
        description="Full API description with Markdown",
        routes=app.routes,
    )
    # add a custom security scheme
    schema["components"]["securitySchemes"] = {
        "ApiKey": {"type": "apiKey", "in": "header", "name": "X-API-Key"}
    }
    app.openapi_schema = schema
    return schema

app.openapi = custom_openapi

Rule of thumb: override app.openapi() only for cross-cutting schema changes (logos, extra security schemes); use per-route responses={} for endpoint-level docs.

Pass openapi_tags to FastAPI(). Each entry maps a tag name to a description and optional external documentation link.

tags_metadata = [
    {"name": "users", "description": "Operations with users. The **login** flow."},
    {"name": "items", "description": "Manage items. So _fancy_", "externalDocs": {
        "description": "More docs", "url": "https://example.com"
    }},
]

app = FastAPI(openapi_tags=tags_metadata)

Tags that appear in routes but not in openapi_tags still show up in the docs — they just won't have descriptions.

Rule of thumb: define openapi_tags whenever the API has more than three resource groups — it makes the Swagger UI navigable for external consumers.

Declare an OAuth2PasswordBearer or HTTPBearer security scheme. FastAPI adds the "Authorize" button to Swagger UI automatically.

from fastapi.security import HTTPBearer

bearer = HTTPBearer()

@app.get("/secure", dependencies=[Depends(bearer)])
async def secure_endpoint():
    return {"ok": True}

For a full OAuth2 password flow with the Swagger "Authorize" dialog:

from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")

@app.get("/me")
async def me(token: str = Depends(oauth2_scheme)):
    ...

Rule of thumb: using OAuth2PasswordBearer (not just HTTPBearer) gives you the full username/password login form in Swagger UI — useful for manual testing.

FastAPI generates OpenAPI 3.1.0 schemas by default as of v0.99+ (previously 3.0.x). OpenAPI 3.1 uses JSON Schema 2020-12 for component schemas, which means it properly supports null types, $ref alongside other keywords, etc.

app = FastAPI()
# GET /openapi.json → {"openapi": "3.1.0", ...}

If you need 3.0.x compatibility (e.g., older code generators):

app = FastAPI(openapi_version="3.0.3")

Rule of thumb: stay on 3.1.0 for new projects; pin to 3.0.x only if your toolchain doesn't support 3.1 yet.

In Pydantic v2, use model_config with json_schema_extra:

from pydantic import BaseModel, ConfigDict

class Item(BaseModel):
    model_config = ConfigDict(
        json_schema_extra={
            "examples": [{"name": "Widget", "price": 9.99}]
        }
    )
    name: str
    price: float

For field-level examples, use Field(examples=[...]):

from pydantic import Field

class Item(BaseModel):
    name: str = Field(examples=["Widget", "Gadget"])
    price: float = Field(gt=0, examples=[9.99])

Rule of thumb: add at least one realistic example per model — Swagger UI's "Try it out" pre-fills request bodies from examples, saving testers time.

Set include_in_schema=False in the route decorator. The endpoint still works — it just won't appear in /openapi.json or the docs UI.

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

Common uses: health-check endpoints, internal debug routes, legacy redirects that you don't want to document publicly.

Rule of thumb: use include_in_schema=False for infra/ops endpoints that aren't part of the public API contract.

URL prefix versioning (most common):

v1 = APIRouter(prefix="/v1")
v2 = APIRouter(prefix="/v2")
app.include_router(v1)
app.include_router(v2)

Multiple FastAPI apps mounted with Mount:

from starlette.routing import Mount
v1_app = FastAPI()
v2_app = FastAPI()
app = FastAPI()
app.mount("/v1", v1_app)
app.mount("/v2", v2_app)
# Each sub-app has its own /docs

Header versioning (clean URLs, harder to implement):

@app.get("/items")
async def items(accept_version: str = Header(default="v1")):
    if accept_version == "v2":
        ...

URL prefix is recommended for REST APIs because it's cacheable, bookmarkable and obvious in logs.

Rule of thumb: use URL prefix (/v1, /v2) with separate APIRouter instances per version; mounted sub-apps are the cleanest option when versions diverge significantly.

Add the headers to the responses dict in the route decorator under the status code's headers key:

@app.get(
    "/items",
    responses={
        200: {
            "headers": {
                "X-Total-Count": {
                    "description": "Total number of items",
                    "schema": {"type": "integer"},
                }
            }
        }
    },
)
async def list_items(response: Response):
    items = await db.all()
    response.headers["X-Total-Count"] = str(len(items))
    return items

Rule of thumb: document custom response headers in responses={} so consumers know to look for them; set them at runtime by injecting Response.

Callbacks document webhooks that your API will call on the consumer's server after a certain event. They're outbound HTTP calls your API makes, documented as if they were inbound routes.

from fastapi import APIRouter

invoicing_callback = APIRouter()

@invoicing_callback.post("{$callback_url}/invoice")
def invoice_notification(body: InvoiceEvent): ...

@app.post("/subscriptions", callbacks=invoicing_callback.routes)
async def create_subscription(subscription: Subscription):
    # after payment succeeds, your server will POST to subscription.callback_url
    ...

Rule of thumb: define callbacks when your API sends webhook notifications — it lets consumers generate typed handlers for the events you'll push to them.

Pass swagger_ui_init_oauth to FastAPI():

app = FastAPI(
    swagger_ui_init_oauth={
        "clientId": "my-client-id",
        "scopes": "openid profile email",
        "usePkceWithAuthorizationCodeGrant": True,
    }
)

This pre-fills the "Authorize" dialog in Swagger UI so developers don't have to type the client ID on every test session.

For full Swagger UI parameter control:

app = FastAPI(
    swagger_ui_parameters={
        "deepLinking": True,
        "persistAuthorization": True,   # keeps auth token across page refreshes
    }
)

Rule of thumb: set persistAuthorization: True in development environments so testers don't lose their JWT token every time they reload Swagger UI.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel