Skip to content

Request Lifecycle Interview Questions & Answers

14 questions Updated 2026-06-20 Share:

FastAPI request lifecycle interview questions — middleware chain, routing, dependency resolution, validation, handler execution and response serialization.

14 of 14
  1. Uvicorn accepts the TCP connection and parses HTTP/1.1 or HTTP/2 bytes into an ASGI scope + receive/send pair.
  2. Middleware stack (outermost first) wraps the request. Each middleware can short-circuit or modify request before passing to the next layer.
  3. FastAPI router matches the URL path + HTTP method to a route handler. If no match → 404 HTTPException.
  4. Dependency injection resolves all Depends() in the handler signature, recursively, before the handler runs.
  5. Parameter extraction & Pydantic validation — path params, query params, headers, cookies and the request body are parsed and validated against type annotations. Validation error → 422 RequestValidationError.
  6. Handler executesasync def on the event loop, def in a thread pool.
  7. Response serialization — the return value is filtered through response_model (if set), then serialized to JSON via Pydantic.
  8. Middleware stack (innermost first on the way out) can modify the response.
  9. Uvicorn writes the HTTP response bytes to the socket.

Rule of thumb: middleware wraps everything; dependency injection happens before validation; validation happens before the handler body.

Middleware added with app.add_middleware() wraps the app from the outside in: the last middleware added becomes the outermost layer. For requests, outermost runs first; for responses, innermost runs first (onion model).

app.add_middleware(TimingMiddleware)   # added first → innermost layer
app.add_middleware(AuthMiddleware)     # added second → outermost layer

# Request flow:  AuthMiddleware → TimingMiddleware → route handler
# Response flow: route handler → TimingMiddleware → AuthMiddleware

This mirrors how ASGI middleware chains work in Starlette. FastAPI also applies its own built-in error handler and routing logic inside the inner layers.

Rule of thumb: add logging/timing middleware last so it wraps everything, including auth middleware.

HTTPException is FastAPI's way to abort a request with a specific HTTP status code and detail message. Raising it anywhere in a handler or dependency immediately skips the rest of the stack and returns a JSON error response.

from fastapi import HTTPException

@app.get("/items/{item_id}")
async def get_item(item_id: int):
    item = await db.get(item_id)
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    return item
# Response: {"detail": "Item not found"} with HTTP 404

You can add custom headers for auth challenges:

raise HTTPException(
    status_code=401,
    detail="Not authenticated",
    headers={"WWW-Authenticate": "Bearer"},
)

Rule of thumb: raise HTTPException for expected error conditions (not found, unauthorized); let unhandled exceptions propagate to the global exception handler for unexpected errors.

Use @app.exception_handler(ExcType) to register a handler that catches a specific exception class anywhere in the request chain.

from fastapi import Request
from fastapi.responses import JSONResponse

class InsufficientFundsError(Exception):
    def __init__(self, balance: float):
        self.balance = balance

@app.exception_handler(InsufficientFundsError)
async def funds_handler(request: Request, exc: InsufficientFundsError):
    return JSONResponse(
        status_code=402,
        content={"detail": f"Balance too low: {exc.balance}"},
    )

To override the default validation error handler:

from fastapi.exceptions import RequestValidationError

@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(status_code=422, content={"errors": exc.errors()})

Rule of thumb: use HTTPException for expected HTTP errors; use custom exception handlers to convert domain exceptions into HTTP responses without polluting handlers.

FastAPI returns HTTP 422 Unprocessable Entity with a JSON body that lists every field that failed validation. This is automatic — you don't write any validation code yourself.

{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "email"],
      "msg": "Field required",
      "input": {"name": "Alice"},
      "url": "https://errors.pydantic.dev/2.0/v/missing"
    }
  ]
}

The loc array pinpoints where the bad value came from: ["body", "field"] for body params, ["query", "name"] for query params, ["path", "id"] for path params.

Rule of thumb: 422 = you sent bad data; 400 = request was intentionally rejected by application logic; treat them differently in client error handling.

FastAPI builds a dependency graph at startup by inspecting type annotations of all handlers. At request time it resolves depth-first: sub-dependencies are resolved before the dependencies that need them.

Shared dependencies are called once per request (by default). If two dependencies both declare Depends(get_db), FastAPI calls get_db() once and passes the same object to both.

async def get_db():        # called once even if used by two deps
    async with AsyncSession() as session:
        yield session

async def get_current_user(db = Depends(get_db)):
    ...

async def check_permission(db = Depends(get_db)):
    ...

@app.get("/secure")
async def handler(
    user = Depends(get_current_user),
    perm = Depends(check_permission),  # same db session as above
):
    ...

To opt out of caching (force a fresh call), pass use_cache=False: Depends(get_db, use_cache=False).

Rule of thumb: shared sub-dependencies are singletons per request — rely on this to share a DB session cleanly without a thread-local or global.

  1. If a response_model is set, FastAPI uses Pydantic to validate and filter the return value through that model (strips extra fields, applies aliases, etc.).
  2. The filtered Python object is converted to a JSON-compatible dict via Pydantic's model_dump(mode="json").
  3. The dict is serialized to JSON bytes using orjson (if installed) or the standard json module.
  4. The bytes are sent as Content-Type: application/json.
class UserOut(BaseModel):
    id: int
    name: str
    # no `password` field → never leaked

@app.get("/users/{id}", response_model=UserOut)
async def get_user(id: int) -> User:
    return await db.get(User, id)   # User has password field — filtered out

If no response_model is set, FastAPI calls jsonable_encoder() on the return value, which handles datetime, UUID, Enum etc.

Rule of thumb: always set response_model on endpoints that return ORM objects to prevent accidental data leakage.

BackgroundTasks run after the HTTP response has been sent to the client. The route handler adds tasks; FastAPI sends the response; then the tasks execute sequentially in the same event loop (for async tasks) or thread pool (for sync tasks).

from fastapi import BackgroundTasks

def send_email(to: str, msg: str):
    ...   # slow SMTP call — runs after response is sent

@app.post("/signup")
async def signup(email: str, background_tasks: BackgroundTasks):
    await db.create_user(email)
    background_tasks.add_task(send_email, email, "Welcome!")
    return {"status": "created"}   # response sent immediately

Because they run in the same process, background tasks share memory with the app but have no access to the request object once it's closed.

Rule of thumb: use BackgroundTasks for quick fire-and-forget work (email, analytics); use a dedicated task queue (Celery, ARQ) for retryable or long-running jobs.

jsonable_encoder converts Python objects that aren't natively JSON-serialisable (datetime, UUID, Decimal, Pydantic models, ORM objects) into JSON-compatible Python primitives (str, int, dict, list).

from fastapi.encoders import jsonable_encoder
from datetime import datetime

obj = {"created": datetime(2026, 1, 1), "id": uuid4()}
safe = jsonable_encoder(obj)
# {"created": "2026-01-01T00:00:00", "id": "550e8400-..."}

FastAPI calls it automatically when serializing responses. You need to call it manually when:

  • Storing something in a cache or NoSQL DB as JSON.
  • Passing an object to a JSONResponse constructor directly.
return JSONResponse(content=jsonable_encoder(my_pydantic_model))

Rule of thumb: let FastAPI call jsonable_encoder implicitly through response_model; call it explicitly only when building JSONResponse objects manually.

FastAPI uses the parameter name and type annotation as signals:

Source Signal
Path parameter name appears in the route path string /{name}
Query parameter simple type (str, int, float, bool, Optional) not in path
Request body Pydantic BaseModel subclass (or annotated with Body())
Header annotated with Header()
Cookie annotated with Cookie()
@app.put("/items/{item_id}")
async def update_item(
    item_id: int,          # path  — in "{item_id}"
    q: str | None = None,  # query — simple type, not in path
    item: Item,            # body  — Pydantic model
):
    ...

Rule of thumb: if the name matches the path template, it's a path param; if it's a Pydantic model it's a body; everything else defaults to query.

Pass status_code to the route decorator. FastAPI returns 200 by default for GET and 200 for POST; 201 is the semantic choice for resource creation.

from fastapi import status

@app.post("/items", status_code=status.HTTP_201_CREATED)
async def create_item(item: Item):
    saved = await db.save(item)
    return saved

To set the status code dynamically inside the handler, inject the Response object:

from fastapi import Response

@app.get("/maybe")
async def maybe(response: Response, id: int):
    item = await db.get(id)
    if not item:
        response.status_code = 404
        return None
    return item

Rule of thumb: set status codes at the decorator level for fixed codes; inject Response only when the code depends on runtime logic.

prefix in include_router is a path segment prepended to all routes in that router. The route decorator path is the specific endpoint path within the router. They concatenate.

router = APIRouter()

@router.get("/items")           # partial path
async def list_items(): ...

@router.get("/items/{id}")      # partial path
async def get_item(id: int): ...

app.include_router(router, prefix="/v1")
# registered as: GET /v1/items  and  GET /v1/items/{id}

Setting the full path in the decorator makes the router useless as a module prefix. Putting everything in prefix lets you re-mount the same router at different prefixes (e.g., /v1 and /v2).

Rule of thumb: keep route decorators as relative sub-paths; use prefix in include_router for versioning or top-level resource grouping.

No. By default FastAPI treats trailing slashes as distinct routes and does not redirect. A GET to /items/ returns 404 if only /items is registered.

@app.get("/items")     # only matches /items
async def list_items(): ...

Options:

  1. Register both: @app.get("/items") and @app.get("/items/").
  2. Use the redirect_slashes=False or redirect_slashes=True flag on FastAPI(): app = FastAPI(redirect_slashes=True) — a trailing-slash request is 307-redirected to the non-slash version.
app = FastAPI(redirect_slashes=True)

Rule of thumb: disable automatic trailing-slash handling (redirect_slashes=False) for strict APIs; enable it (True) for user-facing endpoints where clients vary.

FastAPI generates the OpenAPI schema at import/startup time, not per-request. It inspects route decorators, type annotations and Pydantic models once and caches the result. The /openapi.json endpoint just serves this cached dict.

To disable the docs entirely (e.g., in production):

app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)

To move the schema to a non-default path:

app = FastAPI(openapi_url="/api/v1/openapi.json")

Lazy generation (compute only on first request) can be achieved by subclassing:

class LazyFastAPI(FastAPI):
    def openapi(self):
        if not self.openapi_schema:
            self.openapi_schema = get_openapi(...)
        return self.openapi_schema

Rule of thumb: disable openapi_url in production if you don't want to expose your API schema publicly; keep it enabled in staging for team tooling.

More ways to practice

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

or
Join our WhatsApp Channel