FastAPI doesn’t just magically turn your Python code into a web API; it orchestrates a series of well-defined steps that transform an incoming HTTP request into an outgoing HTTP response.
Let’s walk through a typical request for /items/5?skip=0&limit=10 to a FastAPI app.
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
is_offer: Union[bool, None] = None
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: Union[str, None] = None, skip: int = 0, limit: int = 10):
return {"item_id": item_id, "q": q, "skip": skip, "limit": limit}
@app.post("/items/")
async def create_item(item: Item):
return item
When a request hits /items/5?skip=0&limit=10, the first thing FastAPI does is parse the incoming HTTP request. This involves extracting the HTTP method (GET), the URL path (/items/5), and any query parameters (skip=0, limit=10). The Host, User-Agent, and other headers are also processed.
Next, FastAPI matches the request path and method against your defined routes. It finds a match with @app.get("/items/{item_id}"). The path parameter item_id is extracted from the URL. Because the route expects an integer for item_id, FastAPI automatically performs type coercion and validation. It converts "5" from the URL into the Python integer 5. If you had requested /items/abc, this step would fail, and FastAPI would return a 422 Unprocessable Entity error.
The query parameters skip and limit are also parsed and validated. FastAPI sees they are defined as int in the function signature and converts "0" to 0 and "10" to 10. The q parameter is optional and not present in the URL, so it defaults to None.
Now, FastAPI has all the necessary pieces: the validated path parameter (item_id=5), the validated query parameters (skip=0, limit=10), and the default value for q (None). It then calls your decorated function, read_item, passing these values as arguments: read_item(item_id=5, q=None, skip=0, limit=10).
Your function executes. In this simple case, it just returns a dictionary. This dictionary is then passed back to FastAPI.
FastAPI takes the returned dictionary and serializes it into JSON. It also determines the appropriate HTTP status code, which defaults to 200 OK for successful GET requests. It then constructs the outgoing HTTP response, setting the Content-Type header to application/json and placing the JSON-encoded dictionary in the response body.
The final response is sent back to the client.
{
"item_id": 5,
"q": null,
"skip": 0,
"limit": 10
}
This entire process highlights FastAPI’s reliance on Python type hints and Pydantic models for automatic data validation and serialization, which is a core part of its design.
Consider a POST request to /items/ with a JSON body like {"name": "Foo", "price": 12.99}. FastAPI parses the request body, and because the create_item function expects a parameter of type Item (a Pydantic model), it uses Pydantic to validate the incoming JSON against the Item schema. It checks if name is a string and price is a float. If the JSON is valid, Pydantic creates an Item instance, which is then passed to your create_item function. If the JSON is invalid (e.g., {"name": 123, "price": "abc"}), FastAPI returns a 422 Unprocessable Entity error with detailed information about the validation failures.
The real power of this flow becomes apparent when you introduce middleware. Middleware can intercept requests before they reach your route handlers or responses before they are sent back to the client. For example, you could have middleware that logs every incoming request, or middleware that adds a custom header to every outgoing response, or even middleware that performs authentication and rejects requests that don’t meet certain criteria. This allows you to build complex application logic without cluttering your individual route functions.
When a request includes a JSON body for a POST or PUT request, and your endpoint expects a Pydantic model, FastAPI doesn’t just take the JSON. It uses Pydantic to perform a deep validation. This means not only is the top-level structure checked, but any nested models or complex types within the JSON are also validated according to their definitions. If there’s a mismatch – say, an integer where a string is expected, or a missing required field – Pydantic raises a ValidationError. FastAPI catches this, translates it into a 422 Unprocessable Entity response, and crucially, includes a detailed JSON error body that explains exactly which fields failed validation and why. This is incredibly helpful for debugging client-side issues.
The next step in understanding FastAPI’s request lifecycle is exploring how dependency injection allows you to reuse logic and manage state across your routes.