FastAPI & Python Web Frameworks

How backend concepts like routing and middleware look in Python, using FastAPI as an example of a modern, async, type-driven framework.

What is it?

Everything covered so far — routing, middleware, the request/response lifecycle — isn't specific to Node.js or Express; it's how backend frameworks work in general. Python has its own web frameworks: Flask and Django are the older, synchronous-first options, while FastAPI is a newer framework built around Python's type hints and async/await.

FastAPI's defining feature is that it uses ordinary Python type hints to do double duty: a Pydantic model describing a request body automatically validates incoming data (rejecting anything that doesn't match, with a clear error) and generates interactive API documentation for free, without writing either by hand.

Explain like I'm 10

It's the same directory board from routing, just installed in a different building — the underlying idea (map a method and path to a function) is identical, only the syntax and a few extra conveniences differ.

Examples

A route with a path parameter and a request body

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float

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

@app.post("/items")
async def create_item(item: Item):
    return {"created": item.name, "price": item.price}

Declaring item_id: int makes FastAPI convert and validate it automatically; declaring the Item Pydantic model does the same for the whole request body, rejecting requests missing name or price with a 422 error before your function even runs.

Dependencies — FastAPI's take on shared, cross-cutting logic

from fastapi import Depends, FastAPI, HTTPException

app = FastAPI()

def get_current_user(token: str):
    if token != "valid-token":
        raise HTTPException(status_code=401, detail="Invalid token")
    return {"user": "alice"}

@app.get("/profile")
async def profile(user: dict = Depends(get_current_user)):
    return user

Depends() is FastAPI's equivalent of Express middleware for things like auth checks — instead of running before the handler in a chain, it's declared as a parameter the handler needs, and FastAPI resolves it before calling the route.

Real middleware — running for every request, unconditionally

import time
from fastapi import FastAPI, Request

app = FastAPI()

@app.middleware("http")
async def add_timing_header(request: Request, call_next):
    start = time.time()
    response = await call_next(request)
    duration = time.time() - start
    response.headers["X-Process-Time"] = str(duration)
    return response

This is FastAPI's actual middleware — closer to Express's (req, res, next) chain than Depends() is. It runs for every single request regardless of which route matches, and call_next(request) is exactly like calling next() in Express: it hands control onward and gives you a chance to act again once the response comes back.

How it works

FastAPI is built on Starlette for the actual async web layer (an ASGI application, run by a server like Uvicorn) and Pydantic for data validation. When a request arrives, FastAPI matches the path to a decorated function, uses the function's type hints and any Pydantic models to validate and parse path parameters, query strings, and the body before ever calling your function, then serializes whatever you return back to JSON automatically. Those same type hints are used to generate an OpenAPI schema, which powers the interactive docs served at /docs.

Why does it exist?

Flask and Django predate widespread async support in Python and require validation to be written by hand. FastAPI exists to bring Node-style non-blocking I/O performance to Python, while using type hints — which Python developers were already writing for other reasons — to eliminate most of the boilerplate around validating requests and documenting an API.

When to use it

FastAPI is a strong choice when a team is already in the Python ecosystem (common alongside data or ML work) and wants async performance, strict request validation, and free interactive API docs without extra tooling.

When not to use it

If a team and its libraries are already committed to Node, or the app is a simple, mostly synchronous CRUD site where Flask or Django's simpler, more batteries-included conventions are enough, introducing FastAPI's async model and Pydantic layer is unnecessary complexity.

Common mistakes

  • Calling a blocking, non-async library (like an old synchronous database driver) inside an async def route without awaiting an async-compatible alternative, which stalls the entire event loop for every other concurrent request.

  • Skipping Pydantic models and accepting a raw, untyped request body, losing both automatic validation and the automatically generated documentation.

  • Treating Depends() as identical to Express-style middleware — it's closer to an injectable parameter the handler declares it needs, rather than a step that always runs before the handler in a fixed chain.

Practice exercises

  1. Easy:

    Write the FastAPI equivalent of an Express route: GET /ping returning {"status": "ok"}.

  2. Medium:

    Define a Pydantic model CreateUser with a name (str) and age (int), and a POST /users route that accepts it and returns the values back.

  3. Hard:

    Explain why calling a blocking, synchronous database call inside an async def route handler can slow down every other concurrent request in FastAPI, not just the one making that call.

Interview questions

What is FastAPI built on?

Starlette (an ASGI framework) for the async web layer, and Pydantic for data validation and serialization.

How does FastAPI generate interactive API documentation automatically?

It builds an OpenAPI schema from the same type hints and Pydantic models used to validate requests, and serves an interactive UI from that schema at /docs.

What's the risk of a blocking call inside an async route handler?

It occupies the single event loop thread, delaying every other concurrent request, the same way a CPU-heavy synchronous operation would block Node.js.

What's the difference between Flask/Django and FastAPI in terms of built-in async support?

Flask and Django originated before async/await was standard in Python and are synchronous-first by design; FastAPI was built from the start around ASGI and async/await, making asynchronous request handling a first-class, native part of the framework rather than an add-on.

What is ASGI, and how does it differ from WSGI, which Flask and Django traditionally use?

WSGI is a synchronous standard interface between Python web apps and servers, handling one request at a time per worker; ASGI extends that model to support async/await and long-lived connections, like WebSockets, letting a single worker handle many concurrent requests without blocking on I/O.

What does declaring a path parameter as `item_id: int` actually cause FastAPI to do, beyond documentation?

FastAPI actively validates and converts the incoming string path segment into an actual Python int before calling the route function, and automatically returns a 422 error if the value can't be converted — the type hint is enforced behavior, not just a comment for humans.

What is a Pydantic model, and what two things does defining one for a request body give you at once?

A Python class declaring expected fields and their types; used as a FastAPI request body parameter, it both validates and parses incoming JSON against that shape, rejecting anything that doesn't match with a 422, and feeds the same declaration into the automatically generated OpenAPI docs.

What's the difference between `Depends()` and Express-style middleware, conceptually?

Express middleware runs unconditionally in a fixed chain before every matching route, regardless of what that specific route needs; Depends() is declared as a parameter a specific route function asks for, and FastAPI resolves only the dependencies that route actually declares — closer to dependency injection than to a universal pipeline stage.

How does FastAPI's real `@app.middleware("http")` differ from `Depends()`?

It runs for every single request regardless of route, and explicitly wraps the call to the next stage via call_next(request), letting it act both before and after the response is generated — much closer to Express's (req, res, next) chain than Depends(), which is scoped to whichever specific routes declare it as a dependency.

What does declaring a route function `async def` do if the code inside never actually uses `await`?

Nothing beneficial by itself — declaring a route async without ever awaiting anything inside it doesn't make it non-blocking; if it does synchronous, blocking work, that work still ties up the single event loop exactly as if it weren't declared async at all.

Why can a single blocking, non-async database call inside an `async def` route stall other, unrelated concurrent requests in FastAPI?

FastAPI runs on a single event loop per worker process, cooperatively switching between concurrent requests only at await points; a blocking call has no await point and runs to completion on that same thread, so the event loop can't switch to any other request's work until it finishes, effectively pausing every other concurrent request too.

What's one way to run a genuinely blocking library inside a FastAPI route without stalling the event loop?

Run it in a separate thread pool via something like run_in_threadpool or asyncio.to_thread, so the blocking call executes off the main event loop thread and the route can still await its result without freezing other concurrent requests in the meantime.

What HTTP status code does FastAPI return by default when a request body fails Pydantic validation, and why that code specifically?

422 Unprocessable Entity — the request is well-formed as HTTP/JSON, unlike a genuinely malformed 400, but its content doesn't satisfy the semantic rules the endpoint requires, which is exactly the distinction 422 is meant to convey.

How would you raise a custom error response, like a 404, from inside a FastAPI route function?

Raise HTTPException(status_code=404, detail="...") — FastAPI catches this specific exception type and converts it into the corresponding HTTP response automatically, without needing centralized middleware to catch it.

Why might a dependency declared with `Depends()` be reused across multiple different routes?

It's just a regular Python callable, so the exact same dependency function, like get_current_user, can be imported and declared by any number of route functions that need that logic, without duplicating its implementation — FastAPI resolves it independently for each route that asks for it.

What's a benefit of typing a query parameter, e.g. `q: str | None = None`, rather than manually reading `request.query_params`?

FastAPI automatically parses, validates, and converts the query parameter according to the declared type, applies the default if it's missing, and includes it in the generated docs — manually pulling from request.query_params would require writing that same conversion, default-handling, and validation logic by hand.

What's the relationship between a FastAPI `APIRouter` and the main `app`, when a project splits routes into separate files?

An APIRouter lets a group of related path operations be defined in a separate module and later attached to the main FastAPI app via app.include_router(...), mirroring the way an Express app might mount a sub-router, keeping the main app file from having every single route defined directly in it.

Why might a team choose Flask or Django over FastAPI for a simple, mostly synchronous CRUD app?

Their simpler, more batteries-included, synchronous conventions are enough for that kind of app, and introducing FastAPI's async model and Pydantic-based validation layer would add complexity without a corresponding benefit if the app was never going to need high concurrency or the kind of over-the-wire validation that layer is built for.