Inside a request: how FastAPI turns a signature into behavior

FastAPI's central request-time question is simple: when an HTTP request arrives, how does the framework know where each value belongs before it calls a Python function? This chapter follows that question through signature analysis, input extraction, Pydantic validation, error reporting, and endpoint dispatch. The practical result is a way to read a FastAPI signature as a map of the request boundary rather than as a list of unrelated parameters.

Before the function runs: interpreting the signature

A route declaration gives FastAPI two kinds of information. The decorator identifies an HTTP method and a path pattern. The function signature describes the values the function expects. FastAPI combines those descriptions when the route is registered, before any particular client request is processed.

Start with a path such as /items/{item_id} and a function parameter named item_id. The matching name is significant: FastAPI treats that value as a path parameter. If the URL contains /items/42, the string 42 is not passed blindly to the function. It is associated with the declared field and validated against the int annotation. If the value is /items/not-a-number, the request fails at the boundary instead of reaching code that assumes it already has an integer.

Parameters that do not match a path placeholder are classified using their declarations. A scalar type such as str, int, float, or bool is normally interpreted as a query parameter. A non-scalar annotation, especially a Pydantic model, is normally interpreted as a request body. The word “normally” matters. FastAPI also provides explicit metadata such as Path, Query, Header, Cookie, Body, Form, and File, so the application can state the intended source when inference is not enough or when the HTTP representation needs extra constraints.

That explicit metadata is where a simple convention becomes a deliberate API design. A query field can carry a description, an alias, or a length constraint. A header field can use the name expected by the HTTP client even when the Python name follows a different convention. A body or form field can declare its media type. The details vary by field, but the principle is stable: the annotation says what the value means in Python, while the FastAPI field declaration can say how that value enters the HTTP operation.

Defaults also matter independently of type annotations. A parameter annotated as nullable is not automatically optional merely because its type allows None; its default and field metadata determine whether the request must provide it. Conversely, a default can make a query parameter optional while the annotation still tells editors and validators what type to expect when it is present. Keeping “what type is this?” separate from “must the client send it?” prevents a common misunderstanding of compact signatures.

The result is not just a table of names. FastAPI creates typed fields that retain the annotation, default, alias, validation rules, and location. It also builds a description of the endpoint's dependencies and special inputs. A request object, response object, background-task container, or security scope is not treated like a JSON field; FastAPI recognizes these special types and supplies them through the request handler. This is why the framework can use one function signature to coordinate values from several parts of an HTTP request.

Consider a single operation that accepts item_id: int, an optional q: str, and an Item model. The first value comes from the path, the second from the query string, and the third from the JSON body. The function can receive all three as ordinary Python arguments, but the client sends them through different transport locations. FastAPI's job before invocation is to keep those locations distinct, apply the right field rules, and assemble the values into the call.

That classification also affects the public description. A path parameter belongs in the operation's path-level parameters. A query parameter has a different location and requiredness. A Pydantic body becomes a JSON Schema definition and a request-body schema. The same information used to prepare runtime extraction can later appear in OpenAPI. A declaration therefore connects the wire representation to the Python representation without requiring the endpoint to manually parse a dictionary of raw values.

sequenceDiagram participant C as Client participant A as ASGI app participant R as FastAPI route participant V as Pydantic validation participant E as Endpoint C->>A: Send HTTP request A->>R: Match path and method R->>R: Interpret route signature R->>V: Extract path, query, and body fields alt Invalid input V-->>R: Return location-aware errors R-->>C: Send validation response else Valid input V-->>R: Return converted values R->>E: Call endpoint with values E-->>R: Return application result R-->>C: Continue response handling end

The sequence has an important ordering. Route matching happens before field validation, and field validation happens before the endpoint call. The diagram does not show response serialization in detail because that is a separate boundary: the endpoint's result still has to become a public HTTP response. What matters here is that a malformed request is stopped before application code receives it.

From raw request to a safe endpoint call

The request handler begins by reading the representations that the declared fields require. For a JSON body, it reads the request body and parses JSON when the content type identifies a JSON media type. For forms, it obtains form data and handles file values according to the declared field. Path, query, header, and cookie values come from their corresponding request collections. The handler does not treat all incoming text as interchangeable: the declaration determines which collection is consulted and which conversion rules apply.

This distinction is especially important for forms and files. A form field is not simply a JSON object sent with a different spelling, and an uploaded file may need to be read or represented as a file object rather than converted like a scalar. FastAPI's field metadata tells the request handler which extraction path to use. The endpoint still receives a Python value, but the route declaration preserves the transport fact that produced it.

Pydantic model fields are the bridge between raw transport data and Python values. Suppose an Item model declares a required name as a string, a required price as a floating-point number, and an optional tax. FastAPI gives the received body to the model field for validation. If the client sends a numeric representation that Pydantic can convert under the model's rules, the endpoint receives a model instance with the declared shape. If a required value is absent or cannot be validated, the model returns an error rather than an object the endpoint would have to inspect manually.

The model does more than check top-level keys. Pydantic supports nested structures and typed collections, so a model can describe a hierarchy of data rather than forcing the endpoint to walk raw dictionaries. That is useful because the same nested shape can be represented in editor support and in JSON Schema. It also means that a seemingly small body declaration can define a substantial boundary; the clarity of the model becomes part of the API's maintainability.

The same idea applies to singular parameters. A path value arrives as text because URLs are textual, but the field for item_id: int validates and converts it before the function is called. A query parameter can be required or optional based on its default and field declaration. Headers can use aliases that reflect HTTP naming conventions. These are small transport details, but handling them consistently is what lets endpoint code work with meaningful Python values instead of repeating parsing rules.

FastAPI accumulates errors with their locations. An invalid integer can be reported as a problem in the path, while a missing model field can be reported under the body and the field name. A malformed query value is not confused with a malformed JSON body. This location information is useful to both humans and clients because it identifies the boundary at which the request failed. Applications can use FastAPI's default validation handler or install a custom handler, but changing the presentation does not change the underlying distinction between sources.

There is a difference between parsing failure and field-validation failure, even when both happen before the endpoint. If a request body cannot be decoded as JSON, the handler has no usable document to validate. If the JSON is well-formed but a required field is missing, the body exists and the model can report the missing location. FastAPI's error model keeps these cases in the request-validation path while preserving enough context for an application to explain the problem or log it safely.

The error gate is the decisive transition. FastAPI solves the declared inputs and dependencies, collects validation errors, and checks whether errors remain. If they do, it raises a request-validation error and does not call the endpoint. That protects the function from having to handle every possible missing or malformed transport value on its own. It also creates a stable expectation for clients: invalid input is rejected as a request problem rather than producing an arbitrary failure deep inside application logic.

This does not mean every failure becomes a client error. A body can be valid JSON and still violate a business rule. A syntactically valid identifier may refer to a record the caller is not allowed to access. A value may satisfy a model's range but conflict with a database constraint. Those decisions require application code, dependencies, a database, or an authorization policy. FastAPI's validation boundary guarantees only the rules that have actually been declared there.

Once the values are valid, FastAPI calls the endpoint with the assembled arguments. The endpoint can be a regular function or an async def function. If it is asynchronous, FastAPI awaits it so it can pause while an awaitable database or network operation is in progress. If it is a regular synchronous function, FastAPI runs it in an external threadpool rather than calling it directly on the event-loop path. The distinction is not a style preference detached from behavior: it should match the libraries the endpoint calls.

For example, an endpoint using an asynchronous database client can use async def and await the client operation. An endpoint using a blocking library can remain a normal def; FastAPI's dispatch machinery accommodates it. That accommodation prevents a synchronous endpoint from directly blocking the event loop, but it does not make blocking work costless. Threads still consume resources, and a CPU-heavy computation is not transformed into efficient asynchronous I/O merely because it sits behind a FastAPI route.

The same dispatch rule applies to dependencies, even though the dependency graph is the next layer of the system. A synchronous dependency can run in the threadpool and an asynchronous dependency can be awaited. This is one reason FastAPI can combine ordinary Python libraries with async libraries in a single application. It also creates a responsibility for the developer: a function that performs blocking work should be declared and placed with an honest understanding of that work, not based on the appearance of the endpoint alone.

The boundary validation does not cross

A useful test of this mechanism is to ask what the endpoint can safely assume after it begins. It can assume that FastAPI's declared transport fields passed their validation rules. It cannot assume that the caller is authorized, that a referenced object exists, that a transaction will succeed, or that the returned data is already safe to expose. Those are different questions with different owners.

This separation prevents two opposite mistakes. The first is putting too much parsing into every endpoint: each function manually reads headers, converts numbers, checks missing keys, and formats errors. FastAPI is designed to centralize those transport concerns in declarations and field validation. The second is expecting declarations to encode the entire domain: a type annotation cannot decide whether a user may alter another user's item, and a valid Pydantic model cannot prove that a database write is conflict-free.

The practical reading rule is therefore precise. When looking at a parameter, ask which HTTP location supplies it and which declared field validates it. When looking at the function body, ask which domain decisions remain after that gate. When looking at a failure, ask whether it happened while locating or validating transport data, while executing application logic, or while producing the response. The same route can cross all three boundaries, but FastAPI's request pipeline keeps their first transition explicit.

The next problem follows naturally. Many operations need the same token extraction, current-user lookup, database session, or shared query parameters. Copying those concerns into each endpoint would preserve the request pipeline but lose the benefit of a single declaration. FastAPI's dependency graph supplies the next layer: it lets shared policy and resources become declared inputs that are solved before the endpoint runs.

References