Skip to content

Path Operations Interview Questions & Answers

15 questions Updated 2026-06-20 Share:

FastAPI path operations interview questions — decorators, HTTP methods, status codes, operation metadata, tags, deprecated routes and include_router.

15 of 15

A path operation is the combination of an HTTP method and a URL path bound to a Python function via a decorator. FastAPI registers it as a route and auto-generates the OpenAPI schema entry for it.

@app.get("/items")           # GET  /items  → list_items
async def list_items():
    return [{"id": 1}]

@app.post("/items")          # POST /items  → create_item
async def create_item(item: Item):
    return item

Each decorator (@app.get, @app.post, @app.put, @app.patch, @app.delete, @app.head, @app.options, @app.trace) corresponds to an HTTP method. The same path can be registered for multiple methods.

Rule of thumb: one decorator = one path operation = one OpenAPI endpoint entry.

FastAPI mirrors standard HTTP methods:

Decorator Method Typical use
@app.get GET Read resource
@app.post POST Create resource
@app.put PUT Full update/replace
@app.patch PATCH Partial update
@app.delete DELETE Remove resource
@app.head HEAD Like GET but no body
@app.options OPTIONS CORS preflight / introspection
@app.trace TRACE Diagnostic loopback
@app.patch("/items/{id}")
async def update_partial(id: int, patch: ItemPatch):
    ...

Rule of thumb: use PUT for full replacement, PATCH for partial updates — FastAPI doesn't enforce semantics, but your OpenAPI schema and clients will reflect the choice.

The operation_id is a unique string identifier for an OpenAPI operation. Code generators (openapi-generator, orval) use it as the function name in generated clients. FastAPI auto-generates it from the handler function name and the path, but you can override it.

@app.get(
    "/items/{item_id}",
    operation_id="retrieve_item",   # generated client function: retrieve_item()
)
async def get_item(item_id: int):
    ...

Without an explicit operation_id, FastAPI generates something like get_item_items__item_id__get — verbose and brittle across renames.

Rule of thumb: set explicit operation_id values for any endpoint consumed by generated client code to keep client method names stable across refactors.

tags is a list of strings that group related operations in the Swagger UI and ReDoc docs. They have no effect on routing — they're purely for documentation.

@app.get("/users", tags=["users"])
async def list_users(): ...

@app.post("/users", tags=["users"])
async def create_user(): ...

@app.get("/orders", tags=["orders"])
async def list_orders(): ...

You can also set tags at the APIRouter level so every route in the router inherits them:

router = APIRouter(prefix="/users", tags=["users"])
app.include_router(router)

Rule of thumb: set tags at the router level for consistency; override per-route only when a route genuinely belongs to a different OpenAPI group.

Pass deprecated=True to the route decorator. FastAPI adds the OpenAPI deprecated: true flag, which Swagger UI displays with a strikethrough.

@app.get("/old-endpoint", deprecated=True)
async def old_endpoint():
    return RedirectResponse("/new-endpoint", status_code=301)

Deprecating in OpenAPI doesn't prevent the route from being called — you still need to keep the handler working. For hard removal, emit a Deprecation header with a sunset date, then delete the route in a future release.

Rule of thumb: always mark deprecated routes in OpenAPI before removing them so generated clients surface the warning during their build step.

Use responses dict in the route decorator to document non-default status codes. The summary and description parameters document the operation itself.

@app.get(
    "/items/{id}",
    summary="Retrieve an item",
    description="Returns the full item record. Returns 404 if the item does not exist.",
    responses={
        404: {"description": "Item not found"},
        200: {"description": "The item", "model": Item},
    },
)
async def get_item(id: int):
    ...

FastAPI also reads the handler's docstring as the operation description:

@app.get("/items/{id}")
async def get_item(id: int):
    """Return a single item by its numeric ID."""
    ...

Rule of thumb: use the docstring for prose descriptions; use the responses dict to document error codes and their schemas.

Use the responses parameter with a dict keyed by status code. For typed bodies pass "model": PydanticModel.

from fastapi.responses import JSONResponse
from typing import Union

class Item(BaseModel): id: int; name: str
class Message(BaseModel): message: str

@app.get(
    "/items/{id}",
    response_model=Item,                      # 200 default
    responses={404: {"model": Message}},      # 404 documented
)
async def get_item(id: int) -> Union[Item, JSONResponse]:
    item = await db.get(id)
    if not item:
        return JSONResponse(
            status_code=404,
            content={"message": "not found"},
        )
    return item

Note: FastAPI only validates the response_model; additional entries in responses are documentation-only.

Rule of thumb: document all realistic non-200 responses in responses={} even if FastAPI won't validate them — clients and SDK generators rely on the schema.

FastAPI matches routes in the order they are registered. The first matching route wins. If a fixed path like /items/export is registered after a parameterised path like /items/{id}, the {id} route captures the string "export" as the value of id.

# WRONG order — /items/export matches the first route with id="export"
@app.get("/items/{id}")
async def get_item(id: int): ...   # FastAPI raises 422 because "export" is not int

@app.get("/items/export")
async def export_items(): ...      # never reached

# CORRECT order — fixed paths before parameterised
@app.get("/items/export")
async def export_items(): ...

@app.get("/items/{id}")
async def get_item(id: int): ...

Rule of thumb: register fixed/static paths before parameterised ones at the same depth — or use type constraints ({id:int}) to let FastAPI discriminate automatically.

FastAPI uses Starlette's path converters in the URL string to constrain path parameters without Pydantic at the routing layer:

@app.get("/items/{item_id:int}")    # only matches if segment is an integer
async def get_item(item_id: int): ...

@app.get("/files/{file_path:path}") # matches any path, including slashes
async def get_file(file_path: str): ...

Converters: str (default), int, float, uuid, path.

Combining with Pydantic type annotations provides two-level validation: Starlette rejects non-int URLs at routing time; Pydantic further validates range or constraints in the handler.

Rule of thumb: use :int or :uuid converters in paths to prevent wrong-type segments from even reaching the handler — cleaner 404 instead of 422.

APIRouter is FastAPI's mechanism for splitting route definitions across multiple files. Routes are registered on the router, then the router is mounted onto the app (or another router) with a prefix and shared config.

# routers/users.py
from fastapi import APIRouter
router = APIRouter(prefix="/users", tags=["users"])

@router.get("/")
async def list_users(): ...

@router.get("/{id}")
async def get_user(id: int): ...
# main.py
from routers import users
app.include_router(users.router)
# registers: GET /users/  and  GET /users/{id}

Benefits: logical grouping, reusable prefix/tags/dependencies, testable in isolation, avoids a 1000-line main.py.

Rule of thumb: one APIRouter per resource or domain concept; mount all routers in main.py.

Pass dependencies=[Depends(fn)] to APIRouter() or include_router(). Every route in that router runs the dependency before the handler — useful for auth or rate-limiting.

from fastapi import APIRouter, Depends
from .auth import verify_token

# Option A: on the router itself
router = APIRouter(
    prefix="/admin",
    dependencies=[Depends(verify_token)],
)

# Option B: at mount time (non-destructive)
app.include_router(router, dependencies=[Depends(verify_token)])

Both options stack — if you set deps in both places, both run. Router-level deps run before route-level deps.

Rule of thumb: put auth/rate-limiting deps at the router level so you can't accidentally forget them on a new route; put business-logic deps at the route level.

FastAPI uses the return type annotation in two ways:

  1. OpenAPI schema — the annotation becomes the documented response schema.
  2. Response validation — if response_model is also set, it overrides; if not, FastAPI uses the return annotation as the implicit response_model.
class UserOut(BaseModel):
    id: int
    name: str

@app.get("/users/{id}")
async def get_user(id: int) -> UserOut:   # annotation drives the schema
    return await db.get(User, id)

Since Pydantic v2 + FastAPI 0.100+, you can return an ORM object that matches UserOut and FastAPI will serialise it. Using response_model=UserOut in the decorator is equivalent but takes precedence.

Rule of thumb: use return type annotations as the primary way to document response schemas — it's more Pythonic and keeps the decorator line shorter.

Set status_code=204 in the decorator and return None (or nothing). FastAPI sends no body for 204 responses.

from fastapi import status
from fastapi.responses import Response

@app.delete("/items/{id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(id: int):
    await db.delete(id)
    # implicit return None → no body sent

Alternatively, return Response(status_code=204) explicitly when you need to avoid FastAPI trying to serialise None.

Rule of thumb: always use 204 for successful DELETE operations that return no body — clients rely on the status code to know there's nothing to parse.

Use @app.websocket("/path") and declare the handler with a WebSocket parameter. The handler stays alive in a loop, sending and receiving messages until the connection closes.

from fastapi import WebSocket

@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
    await ws.accept()
    try:
        while True:
            data = await ws.receive_text()
            await ws.send_text(f"Echo: {data}")
    except WebSocketDisconnect:
        pass   # client closed the connection

WebSocket handlers can use Depends() just like HTTP handlers, making it easy to authenticate the upgrade request before ws.accept().

Rule of thumb: always call await ws.accept() before sending; catch WebSocketDisconnect to clean up resources when the client leaves.

Mount Starlette's StaticFiles on a path:

from fastapi.staticfiles import StaticFiles

app.mount("/static", StaticFiles(directory="static"), name="static")

Requests to /static/logo.png serve ./static/logo.png directly without going through FastAPI's routing. It's handled at the ASGI mount level, so it bypasses middleware that is added after the mount.

For a single-page app (SPA) fallback:

app.mount("/", StaticFiles(directory="dist", html=True), name="spa")

html=True serves index.html for any path not found in the directory.

Rule of thumb: use StaticFiles for assets; for dynamically generated file downloads use FileResponse inside a regular route handler.

More ways to practice

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

or
Join our WhatsApp Channel