Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Implementing Typed Path and Query Parameters with Constraints

Hello! Last lesson, you ran an incident API with GET /incidents and saw that FastAPI documented the route automatically. The handler had no inputs, though, so a client could neither identify one incident nor refine a collection request.

Today you’ll add both kinds of URL input: a path parameter for an incident ID and query parameters for options such as status and result limit. You’ll give them Python types and constraints, then use /docs and HTTP requests to see what FastAPI accepts and rejects. We’ll keep the responses deliberately simple; retrieving stored incidents and implementing full collection behavior come later.


A path value becomes a typed function argument

In GET /incidents/42, the 42 identifies the resource the client is asking about. A route such as /incidents/{incident_id} captures that segment and passes it to a function argument with the same name.

Read the official Path Parameters guide for the conversion and validation behavior behind this small piece of syntax.

Path Parameters - FastAPI

Read FastAPI's explanation of how a path segment becomes a typed Python value, and what happens when conversion fails.

In “Path parameters with types,” start at typed path values. Continue through “Data conversion” and “Data validation,” ending where the guide says where validation failed. Focus on the difference between the text in a URL and the integer received by the handler.

A URL arrives as text, but annotating incident_id: int tells FastAPI to parse it before calling your handler. Thus /incidents/42 supplies the Python integer 42; /incidents/abc cannot supply a valid integer and receives a validation response instead. The annotation is not merely an editor hint: FastAPI also uses it to process requests and generate OpenAPI documentation.

A path parameter is required because the route cannot match without that segment. An annotation of int still permits zero and negative integers, however. For an incident ID that must be positive, we’ll add an explicit bound.


Query values refine a request

The query string begins after ?. For example, in /incidents?status=new&limit=20, status and limit are query parameters; neither is part of the route path. In FastAPI, a function argument whose name does not appear in the path is ordinarily interpreted as a query parameter.

The official Query Parameters guide explains the distinctions we need: type conversion, defaults, and required versus optional inputs.

Query Parameters - FastAPI

Read FastAPI's query-parameter guide to connect URL values with function defaults and required inputs.

In the opening “Query Parameters” section, read from query interpretation. In “Defaults,” read from default values; then, in “Optional parameters,” continue from optional inputs. Finally, in “Required query parameters,” start where the text says defaults and requirements, and read through the example of a missing required value. Focus on what the default changes, as distinct from what the type allows.

These two declarations mean different things:

limit: int = 20
search: str | None = None

Both query parameters may be omitted. If limit is omitted, the handler receives 20; if search is omitted, it receives None. The union str | None describes the Python values the handler can receive, while the default determines what happens when the client supplies no value. Conversely, a query argument with no default is required—even if its type includes None.

A supplied value must still pass validation. ?limit=5 can become an integer; ?limit=many cannot. Defaults do not turn invalid supplied values into valid ones.


Put types and constraints into the incident routes

Types establish the basic shape of an input, but they do not express every useful rule. A result limit should be bounded; a search term may need a reasonable length. Watch this short demonstration of FastAPI’s Query constraints before adding them.

Fast API Tutorial, Part 5: Query Parameters and String Validation

Watch Fast API Tutorial, Part 5: Query Parameters and String Validation by JVP Design for a visual demonstration of length constraints and validation responses.

Watch maximum length as the presenter adds Query and sends an overlong value. Then watch minimum length for the corresponding too-short value. Focus on the rejected HTTP request, not just what Swagger UI displays. The code below uses Annotated to keep constraint metadata separate from Python defaults.

Update src/incident_api/main.py to the following. Keep your existing package and development-server setup; if your earlier incident-status enum already lives in another module, import it rather than creating a second copy.

from enum import Enum
from typing import Annotated

from fastapi import FastAPI, Path, Query


class IncidentStatus(str, Enum):
    NEW = "new"
    ACKNOWLEDGED = "acknowledged"
    RESOLVED = "resolved"


app = FastAPI()


@app.get("/incidents")
def list_incidents(
    status: IncidentStatus | None = None,
    limit: Annotated[int, Query(ge=1, le=100)] = 20,
    search: Annotated[
        str | None,
        Query(min_length=2, max_length=80),
    ] = None,
) -> list[dict[str, str | int]]:
    return []


@app.get("/incidents/{incident_id}")
def get_incident(
    incident_id: Annotated[int, Path(gt=0)],
    include_timeline: bool = False,
) -> dict[str, int | bool]:
    return {
        "id": incident_id,
        "include_timeline": include_timeline,
    }

The declarations each serve a distinct purpose:

  • incident_id is a path parameter because its name appears in /incidents/{incident_id}. Path(gt=0) requires an integer greater than zero.
  • status is an optional query parameter. The enum restricts supplied values to new, acknowledged, or resolved.
  • limit defaults to 20, but a supplied value must be an integer from 1 through 100. ge means “greater than or equal to”; le means “less than or equal to.”
  • search may be omitted. If supplied, its string must be between 2 and 80 characters long.
  • include_timeline is an optional boolean query parameter on the single-incident route. FastAPI converts supported query-string boolean values into a Python bool.

Annotated attaches FastAPI’s Path or Query metadata to a Python type without hiding the function’s default. Notice that search has both str | None and = None: the former describes the handler value; the latter makes omission possible. No default is supplied for incident_id, and a path segment cannot be omitted while still matching this route.

These are input demonstrations, not completed incident operations. The collection handler still returns []; it does not yet filter or limit stored data. The single-incident handler echoes an ID and an option; it does not check whether that ID exists or fetch a timeline. In particular, a 200 from /incidents/42 does not yet establish that incident 42 exists. Keeping that boundary clear prevents a validation demonstration from masquerading as a working retrieval API.


Verify the contract at the HTTP boundary

Start the app as in the previous lesson (uv run fastapi dev, or fastapi dev in your activated environment). Use /docs to inspect the two operations. The single-incident route should show incident_id as a required integer path parameter. The collection route should show optional query inputs, including the allowed status values and the bounds on limit and search.

Then compare a few actual requests. Quote URLs containing & in a shell so the shell does not treat it as an operator:

curl -i 'http://127.0.0.1:8000/incidents/42?include_timeline=true'
curl -i 'http://127.0.0.1:8000/incidents?status=new&limit=10&search=deploy'
curl -i 'http://127.0.0.1:8000/incidents/abc'
curl -i 'http://127.0.0.1:8000/incidents/0'
curl -i 'http://127.0.0.1:8000/incidents?limit=101'
curl -i 'http://127.0.0.1:8000/incidents?status=unknown'
curl -i 'http://127.0.0.1:8000/incidents?search=x'

The first request should return an object containing ID 42 and include_timeline: true. The second should still return [], because filtering has not been implemented. The remaining requests match registered routes but supply invalid inputs, so expect 422 validation errors rather than handler results. In an error body, inspect detail and particularly loc: it identifies whether the rejected value came from path or query, and names the parameter. Exact message wording can vary by installed version.

This is different from the 404 you saw for /incidents/42 last lesson. Then, there was no matching route. Now the route exists, but values such as abc or 0 fail its declared input contract. A syntactically valid positive ID that is absent from storage will need a separate not-found decision once retrieval is implemented.

One routing detail is worth keeping in mind as the API grows: if you later add a fixed route such as /incidents/stats, register it before /incidents/{incident_id}. Otherwise the parameterized route may capture stats as an attempted incident ID.


Takeaways

A path parameter selects a resource through a named URL segment; query parameters supply options without changing the route. FastAPI uses their annotations to convert values, validate them, and document them. Path and Query let you add explicit bounds, while Python defaults determine whether query inputs may be omitted.

Next, you’ll define Pydantic request models for creating and updating incidents. Those models will validate JSON request bodies, complementing the URL-input validation you implemented here.

Can't find a good explanation? Sign up and we'll make it for you

Sign up