Senior 4 min · March 05, 2026

FastAPI Path Parameters and Query Parameters

Master FastAPI parameters: handle path and query data with automatic Pydantic validation, optional arguments, and advanced constraints using Query() and Path().

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Path parameters live inside the route URL with curly braces (e.g., /items/{id}) and identify a specific resource
  • Query parameters are any function arguments not found in the path string — used for filtering, sorting, and pagination
  • FastAPI uses Python type hints to automatically validate, convert, and document all parameters via OpenAPI
  • A type mismatch triggers a structured 422 Unprocessable Entity error before your logic executes
  • Annotated is the modern pattern for combining type hints with Query() or Path() validation without breaking IDE autocompletion
  • Default values dictate requirement: no default = required parameter; with default = optional
Plain-English First

Think of path parameters as the street address of a specific house — they tell FastAPI exactly which resource you want. Query parameters are more like instructions on the delivery note — they tell FastAPI how to filter, sort, or paginate what you get back. FastAPI reads the type annotations you write and acts as a bouncer: if the data doesn't match the expected type, it's rejected at the door with a clear error, before your business logic ever sees it.

Parameter handling is the boundary between untrusted client input and your core business logic. In traditional frameworks, validation is an afterthought — you manually cast strings, check for None, and pray edge cases don't slip through. I've seen that pattern collapse under production load more times than I'd like to admit.

FastAPI inverts this model entirely. By leveraging Python's type hinting system, parameters become a typed contract. FastAPI validates against a schema, converts to the correct Python object, and generates interactive OpenAPI documentation — all from your function signature alone. There is no separate validation layer to synchronize, no schema file to keep in sync with your code.

The distinction between path and query parameters is not cosmetic. Path parameters identify a specific resource — they are hierarchical, always required, and semantically load-bearing. Query parameters modify the representation of that resource — they are non-hierarchical, often optional, and composable. Mixing them up produces confusing APIs that frustrate clients, break caching strategies, and signal a shaky understanding of REST resource modeling to anyone reading your routes.

Path Parameters: Resource Identification

Path parameters exist for one reason: to uniquely identify a resource within your URL hierarchy. They are not filters, they are not options, and they are not toggles. If you find yourself putting something optional into a path segment, that is a design smell worth stopping to examine.

FastAPI supports a special syntax called 'Path Converters' that extend the default matching behavior. The most important is the :path converter — it tells FastAPI to match everything remaining in the URL, including forward slashes. Without it, a path parameter stops at the first / it encounters. This makes :path essential for building file explorers, storage proxies, or any service where the resource identifier is itself a hierarchical path.

Path parameters are inherently required at the routing level. If a client omits a path segment, the route simply does not match — the response is a 404, not a 422. The 422 only appears after route matching succeeds and FastAPI attempts to validate the captured value against your type hint and any Path() constraints. This two-stage process means your validation logic never has to handle a missing path parameter; the router handles that before Pydantic is involved.

io/thecodeforge/params/path_params.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from fastapi import FastAPI, Path
from typing import Annotated

app = FastAPI()


@app.get('/forge/users/{user_id}')
def get_user_by_id(
    # Annotated keeps the base type visible to IDEs and type checkers.
    # Path() attaches validation metadata without polluting the default value.
    user_id: Annotated[
        int,
        Path(
            title='User ID',
            description='The internal registry ID of the user. Must be a positive integer.',
            ge=1,
        ),
    ],
):
    """
    Fetch a single user from the internal forge registry by their numeric ID.
    IDs start at 1 — zero and negatives are rejected at the boundary.
    """
    return {'user_id': user_id, 'context': 'internal_forge_registry'}


# The :path converter is required for any parameter that may contain slashes.
# Without it, /forge/assets/images/branding/logo.png would bind only
# 'images' to file_path and the route would 404 on the remainder.
@app.get('/forge/assets/{file_path:path}')
def get_asset_stream(file_path: str):
    """
    Stream a static asset by its full storage path, including subdirectories.
    file_path captures everything after /forge/assets/ including slashes.
    """
    return {'requested_path': file_path}


# GET /forge/users/42
# -> {'user_id': 42, 'context': 'internal_forge_registry'}

# GET /forge/users/0
# -> 422 Unprocessable Entity (ge=1 constraint violated)

# GET /forge/assets/images/branding/logo.png
# -> {'requested_path': 'images/branding/logo.png'}
Output
{'user_id': 42, 'context': 'internal_forge_registry'}
Path Parameters as Resource Coordinates
  • Path = identity. Query = representation. Blurring this line produces APIs that are hard to cache, hard to document, and hard to reason about six months later.
  • A missing path segment produces 404 (route not matched), not 422 (validation failed). These are different failure modes and must be handled differently in clients.
  • Use ge=1 or gt=0 on numeric IDs to reject zero and negatives at the boundary — before they reach your database layer and produce confusing errors downstream.
  • The :path converter is strictly opt-in. Standard path parameters stop at the first slash by design; the converter is your explicit declaration that slashes are part of the value.
  • Path parameters arrive as strings at the HTTP layer. FastAPI uses your type hint to coerce them — so int user_id does not mean the URL contains a binary integer; it means FastAPI will attempt to parse the string '42' into the Python integer 42.
Production Insight
Path parameter validation runs after route matching but before your function body executes. That ordering is load-bearing.
If a client sends GET /forge/users/0, FastAPI matches the route, captures '0', converts it to the integer 0, then checks the ge=1 constraint — and returns 422 before your database query fires. This is your first real defense against garbage data reaching persistence layers.
I have seen teams skip the ge=1 constraint because 'the database will just return an empty result anyway.' That is technically true, but it wastes a database round-trip, generates misleading query logs, and silently accepts input that your API contract never intended to support. Validate at the boundary. Always.
Key Takeaway
Path parameters are resource coordinates — present, validated, and constrained at the routing boundary before your business logic sees them.
Use the :path converter for slash-containing identifiers. Use Annotated + Path() for clean, IDE-friendly validation metadata. Skip constraint validation and you are just deferring the data quality problem downstream.
Choosing Path Parameter Validation
IfResource ID must be a positive integer
UseUse Annotated[int, Path(ge=1)] — rejects zero, negatives, and non-numeric strings before your function body runs
IfParameter may contain slashes (file paths, nested resource identifiers)
UseUse {param:path} converter in the route string — without it, matching stops at the first slash
IfParameter should have a human-readable description in generated docs
UsePass title and description to Path() — these appear directly in Swagger UI and ReDoc without any extra configuration
IfParameter must conform to a specific format like a prefixed ID or UUID
UseUse Path(pattern='^[A-Z]{3}-[0-9]+$') for format enforcement, but benchmark regex cost on hot paths before shipping

Query Parameters: Filtering and Pagination

Query parameters handle everything that modifies how a resource is represented, rather than which resource you are talking about. Filtering a list by status, sorting results by a field, paginating through a large dataset — these all belong in query parameters. They appear after the ? in the URL, are separated by &, and carry no positional significance. /search?q=fastapi&page=2 is semantically identical to /search?page=2&q=fastapi — order does not matter.

In FastAPI, any function argument that does not appear in the route path string is automatically treated as a query parameter. Providing a default value makes it optional. Omitting the default makes it required — FastAPI will reject requests without it and return a 422 with a clear error pointing at the missing field.

FastAPI also supports multi-value query parameters natively. Define a parameter as list[str] and the client sends the same key multiple times: ?tag=python&tag=web&tag=api. FastAPI collects all values and delivers them as a Python list. There is no custom parsing logic required on your end. If the client sends no values for a list parameter, the result is an empty list — unless you set a default with Query().

One thing worth being deliberate about: the boolean type in FastAPI is more permissive than you might expect. It accepts 'true', '1', 'on', 'yes', 'True' as True and their inverses as False. This is intentional — it makes the API compatible with curl, shell scripts, and frontend libraries that serialize booleans differently. Know this behavior exists so it does not surprise you in testing.

io/thecodeforge/params/query_params.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from fastapi import FastAPI
from typing import Optional

app = FastAPI()


@app.get('/forge/search')
def list_artifacts(
    query: str,                    # Required — no default means the client must provide this
    page: int = 1,                 # Optional — defaults to page 1 if not provided
    tag: Optional[str] = None,     # Optional — defaults to None, filtering is skipped
    active: bool = True,           # FastAPI accepts 'true', '1', 'yes' as True
):
    """
    Search artifacts in the forge registry.

    - query: full-text search term (required)
    - page: pagination offset, 1-indexed (optional, default 1)
    - tag: filter results to a specific tag (optional)
    - active: include only active artifacts when True (optional, default True)
    """
    return {
        'search_term': query,
        'pagination': {'page': page},
        'filter': {'tag': tag, 'active': active},
    }


# GET /forge/search?query=api
# -> {'search_term': 'api', 'pagination': {'page': 1}, 'filter': {'tag': None, 'active': True}}

# GET /forge/search?query=api&page=3&tag=infra&active=false
# -> {'search_term': 'api', 'pagination': {'page': 3}, 'filter': {'tag': 'infra', 'active': False}}

# GET /forge/search
# -> 422 Unprocessable Entity (query is required, no default provided)
Output
{'search_term': 'api', 'pagination': {'page': 1}, 'filter': {'tag': None, 'active': True}}
Default Values Create Implicit Contracts You Have to Maintain
  • A default of page: int = 1 silently accepts requests that omit pagination entirely — clients may not realize they are receiving page 1 of a potentially massive dataset. Consider whether page should be required to force explicit intent.
  • Bare default values like = 1 are not validated against constraints. A client can send page=-5 and your function receives -5. If that matters to your logic, use Query(default=1, ge=1) instead — it enforces the constraint on the default path too.
  • Boolean defaults have broad acceptance rules. active: bool = True accepts 'yes', '1', and 'on' as True. If your frontend sends 'active=YES', that is valid. Document this if your API serves multiple client types.
  • None as a default is an explicit signal that the parameter is optional and that 'no value provided' is a meaningful state. Use it deliberately — do not use None when you mean 'default to some value' and then branch on None inside the function.
Production Insight
Query parameter ordering is irrelevant to FastAPI but it matters enormously for HTTP caching.
CDNs, reverse proxies, and Varnish instances typically cache based on the full URL string. /search?q=fastapi&page=2 and /search?page=2&q=fastapi are different cache keys — they represent the same request but produce two cache entries, halving your effective cache hit rate.
If caching is part of your infrastructure strategy, normalize query parameter order at the API gateway or in your client SDK before requests are issued. A simple alphabetical sort of query keys, applied consistently, can double cache hit rates on read-heavy list endpoints. It is a five-line change with outsized impact.
Key Takeaway
Query parameters are composable, order-independent modifiers. Default values make them optional — but bare defaults bypass constraint validation. Use Annotated + Query() any time you need a business rule enforced at the boundary, not just a type check.
And if caching matters in your stack, normalize query string order at the gateway. It costs almost nothing and pays off immediately.
Query Parameter Design Decisions
IfParameter is optional with a sensible default
UseUse param: type = default_value — but if constraints matter, wrap in Query(default=value, ge=1) to enforce them on the default path too
IfParameter is optional but has no sensible default
UseUse Optional[type] = None or type | None = None (Python 3.10+) — signals intentional optionality to both FastAPI and readers
IfParameter should accept multiple values from the same key
UseUse list[type] type hint; client repeats the key: ?tag=a&tag=b&tag=c. Do not use comma-separated strings unless you add explicit parsing.
IfParameter needs validation beyond type checking
UseUse Annotated[type, Query(default=..., ge=1, le=100, min_length=1)] — constraints are enforced before your function body runs

Complex Validation with Query()

Type hints handle the common case — an integer is an integer, a string is a string. But real production APIs have business rules that go beyond type correctness. A service identifier must start with a specific prefix. A pagination limit must be between 1 and 100. A search term must be at least three characters long. These are not type constraints; they are domain constraints, and they belong at the API boundary, not buried inside your business logic.

The Query() class is how you express those constraints in FastAPI. It accepts ge (greater than or equal), gt (greater than), le (less than or equal), lt (less than), min_length, max_length, and pattern for regular expression matching. Every constraint is enforced by Pydantic before your function body executes — a request that violates any constraint is rejected with a structured 422 response containing the field name, the failed constraint, and a human-readable message.

The Annotated pattern is the right way to combine Query() with your type hint. Annotated[str, Query(min_length=3, pattern='^FORGE-.*$')] keeps the base type (str) visible to IDEs and type checkers, while attaching the validation metadata separately. The older pattern — param: str = Query(min_length=3) — works but conflates the default value position with the Query object, which confuses IDEs and breaks autocompletion in some editors.

One constraint worth measuring before you ship: regex patterns via Query(pattern=...) run on every request. A poorly anchored or backtracking-prone regex on a high-traffic endpoint will show up in your profiler. Always anchor your patterns (use ^ and $), test them against pathological inputs, and benchmark them under load before deploying on hot paths. For simple guards like minimum length or maximum value, prefer the numeric and length constraints over regex — they are faster and the error messages are clearer.

io/thecodeforge/params/validation.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from fastapi import FastAPI, Query
from typing import Annotated

app = FastAPI()


@app.get('/forge/audit-logs')
def get_audit_logs(
    # service_id must start with 'FORGE-' and be at least 3 characters.
    # Annotated keeps str visible to IDEs; Query() attaches the constraints.
    service_id: Annotated[
        str,
        Query(
            title='Service Identifier',
            description='Internal service ID. Must use the FORGE- prefix.',
            min_length=3,
            pattern='^FORGE-.*$',
        ),
    ],
    # limit is optional (defaults to 20) but must be between 1 and 100.
    # Query() on a default value enforces the constraint on every request,
    # including those that omit the parameter and trigger the default.
    limit: Annotated[
        int,
        Query(
            title='Result Limit',
            description='Maximum number of log entries to return. Range: 1–100.',
            ge=1,
            le=100,
        ),
    ] = 20,
):
    """
    Retrieve audit log entries for a specific internal service.

    Both parameters are validated before this function body runs.
    Invalid input never reaches the log query logic.
    """
    return {'service': service_id, 'limit': limit}


# GET /forge/audit-logs?service_id=FORGE-AUTH&limit=50
# -> {'service': 'FORGE-AUTH', 'limit': 50}

# GET /forge/audit-logs?service_id=EXTERNAL-SVC&limit=50
# -> 422 Unprocessable Entity
# -> detail: [{'loc': ['query', 'service_id'], 'msg': 'String should match pattern ...', 'type': 'string_pattern_mismatch'}]

# GET /forge/audit-logs?service_id=FORGE-AUTH&limit=500
# -> 422 Unprocessable Entity
# -> detail: [{'loc': ['query', 'limit'], 'msg': 'Input should be less than or equal to 100', 'type': 'less_than_equal'}]
Output
{'service': 'FORGE-AUTH', 'limit': 20}
Validation as an Executable Contract Layer
  • Each constraint (ge, le, min_length, pattern) compiles into a Pydantic field validator at app startup — not at request time. The overhead per request is minimal.
  • Validation failures produce structured 422 responses with field-level detail. The loc array tells clients exactly which parameter failed. The type field gives them a machine-readable error code. Build your client error handling around these fields.
  • Constraints run at the application boundary — the earliest possible moment. This keeps invalid data out of your service layer, your database queries, and your audit logs.
  • The pattern parameter uses Python re syntax. Always anchor patterns with ^ and $. An unanchored pattern like FORGE-.* will match 'PREFIX-FORGE-AUTH' and that is probably not what you intend.
  • Combine Annotated with Query() for the cleanest possible signature. The type is the type; the validation metadata is the metadata. They live in different positions for a reason.
Production Insight
Regex validation in Query(pattern=...) is evaluated by Pydantic on every matching request. The cost is typically measured in microseconds for simple patterns, but it is not zero.
I have seen regex catastrophic backtracking surface as latency spikes on endpoints processing 10,000 requests per second — patterns that looked innocent in unit tests became CPU bottlenecks under sustained load. Before deploying a pattern constraint on a hot path, test it with re.compile(pattern).match(worst_case_input) in a loop and measure microseconds per call. If it crosses your latency budget, refactor the pattern or enforce the constraint in a pre-validation step.
General rule: use min_length and max_length for simple guards, reserve pattern for format enforcement like UUID shapes or prefixed identifiers where the structure genuinely matters.
Key Takeaway
Query() transforms type hints into executable business rules — string patterns, numeric ranges, and length bounds enforced before your function body runs.
Use Annotated for a clean separation between type and validation metadata. Anchor your regex patterns. Measure constraint cost on hot paths before shipping. And read the 422 detail array — it tells you everything you need to fix a broken client.
● Production incidentPOST-MORTEMseverity: high

Silent 422 Storm — Query Parameter Type Mismatch Brings Down Monitoring Dashboard

Symptom
API gateway latency spiked from 12ms to 800ms within three minutes of the frontend deploy. The dashboard showed empty data panels. No application-level errors appeared in logs — only a wall of 422 status codes in the access logs, which the on-call engineer initially dismissed as a client misconfiguration.
Assumption
The team assumed the database was overloaded or that the new deployment had introduced a query regression. They spent two hours profiling slow queries, checking connection pool exhaustion, and rolling back the backend deploy — none of which had any effect, because the backend was functioning correctly the entire time.
Root cause
A frontend refactor changed the pagination widget to send the string 'last' instead of a numeric page index when the user was on the final page. The FastAPI endpoint declared page: int — correctly rejecting the invalid input with 422. But the dashboard's HTTP client classified all non-2xx responses as transient network errors and retried aggressively with exponential backoff that had a misconfigured ceiling. The result was thousands of 422 responses per second — not from broken business logic, but from a client that refused to accept a permanent failure signal.
Fix
Added client-side input validation in the dashboard to guarantee page values are always integers before the request is issued. Added a specific 422 handler in the HTTP client that raises immediately without retry. Added rate limiting on the API gateway scoped to 4xx response rates per client IP. Added a circuit breaker on the dashboard's HTTP client to halt all retries after three consecutive failures to the same endpoint.
Key lesson
  • 422 errors are correct, expected behavior — but every HTTP client must treat them as permanent failures, not transient ones worth retrying
  • Monitor 422 rates as a first-class signal, completely separate from 5xx. A spike in 422s points to a client-side contract violation; a spike in 5xx points to a server-side failure. Conflating them wastes hours of the wrong kind of debugging
  • Always pair API boundary validation with client-side validation. The API is the last line of defense, not the first — clients should never be sending invalid types to production endpoints
  • Exponential backoff without a hard cap and a jitter strategy is a liability. Every backoff implementation should have a max_retries ceiling and a non-retryable error set that includes 4xx responses
Production debug guideSymptom → Action mapping for common parameter failures5 entries
Symptom · 01
422 Unprocessable Entity on every request to a specific endpoint
Fix
Do not guess — read the response body first. FastAPI's 422 payload includes a detail array with machine-readable entries. Each entry contains loc (the parameter location and name), msg (a human-readable description of the failure), and type (the Pydantic error code). The type field tells you whether it's a type coercion failure, a constraint violation, or a missing required field. Fix the client to match the schema; do not modify the server to accept invalid input.
Symptom · 02
Optional query parameter is always None even when provided in the URL
Fix
Parameter name matching is case-sensitive and exact. Verify the URL key matches the function argument name character-for-character. Also check for URL encoding artifacts — spaces should be %20 in query strings, not raw spaces or + characters unless you are using application/x-www-form-urlencoded encoding. If the name matches, check whether a middleware layer is stripping or rewriting query strings before the request reaches FastAPI.
Symptom · 03
Path parameter captures only part of a URL segment when the value contains slashes
Fix
Standard path parameters stop matching at the first / character. Use the :path converter in the route decorator: @app.get('/files/{file_path:path}'). Without it, a request to /files/images/branding/logo.png will only bind images to file_path and fail to match the rest. The :path converter is opt-in by design — FastAPI does not apply it automatically.
Symptom · 04
List query parameter receives a single item instead of a Python list
Fix
Confirm the type hint is list[str] or List[str], not str. Then confirm the client is repeating the key for each value: ?tag=python&tag=web&tag=api. A single ?tag=python with a list[str] type hint still produces ['python'] — that is correct behavior. If the client is sending a comma-separated string like ?tag=python,web, FastAPI will not split it automatically; you need a custom validator or a preprocessing step.
Symptom · 05
Swagger UI lists a parameter in the wrong category — path vs query
Fix
FastAPI's parameter resolution depends on an exact name match between the curly-brace placeholder in the route string and the function argument name. If you declare @app.get('/users/{user_id}') but the function argument is named id, FastAPI cannot find a matching path placeholder and falls back to treating id as a query parameter. Fix the name to match exactly: user_id in both the route and the function signature.
★ FastAPI Parameter Debug Cheat SheetWhen a parameter endpoint is failing in production, run these checks in order. Each step narrows the failure surface before you touch any code.
Unexpected 422 responses
Immediate action
Read the 422 response body before doing anything else — the detail array tells you the exact field, constraint, and failure reason
Commands
curl -s 'http://localhost:8000/forge/search?query=api&page=abc' | python -m json.tool
curl -s 'http://localhost:8000/openapi.json' | python -c "import sys,json; print(json.dumps(json.load(sys.stdin)['paths']['/forge/search'], indent=2))"
Fix now
Fix the client to send the correct type matching the OpenAPI schema. Never loosen the server-side validation to accommodate a broken client.
Path parameter not matching nested routes with slashes+
Immediate action
Check the route definition for the :path converter — its absence is the most common cause of partial path capture
Commands
grep -n 'path' app/routes/*.py
curl -v 'http://localhost:8000/forge/assets/images/logo.png'
Fix now
Add the :path converter to the route parameter: {file_path:path}. Redeploy and retest with a multi-segment path.
Query parameter ignored or defaulting unexpectedly on every request+
Immediate action
Compare the URL query string key name with the function argument name — case differences are invisible and cause exactly this symptom
Commands
curl -v 'http://localhost:8000/forge/search?query=fastapi&page=2' 2>&1 | grep -i 'page'
python -c "from app.main import app; print([r.path for r in app.routes])"
Fix now
Ensure URL parameter names are exact, case-sensitive matches for function argument names. If the mismatch is in a client SDK, update the SDK and add a contract test to prevent regression.
Path Parameters vs Query Parameters
AspectPath ParameterQuery Parameter
PurposeIdentify a specific resource — answers 'which one?'Filter, sort, or paginate resources — answers 'in what form?'
URL PositionEmbedded in the route path (e.g., /users/{id})After the ? in the URL (e.g., ?page=2&status=active)
RequirementAlways required — a missing segment produces 404 at routing, before validation runsOptional when a default value is provided; required without one
Validation Error422 if captured value fails type conversion or Path() constraints422 if provided value fails type conversion or Query() constraints
Order SensitivityPositional — the first {placeholder} matches the first path segmentOrder-independent — matched by key name, not position
Multiple ValuesNot supported — path segments are singular by definitionSupported via list[type] type hint; client repeats the key in the URL
OpenAPI PlacementDocumented under 'Parameters' with 'in': 'path'Documented under 'Parameters' with 'in': 'query'
Cache ImpactPart of the URL path — a standard, stable cache key componentPart of the query string — cache behavior varies by proxy; normalize key order to maximize hit rates

Key takeaways

1
Hierarchical data that identifies a specific resource belongs in path parameters. Non-hierarchical data that filters, sorts, or paginates belongs in query parameters. This distinction is not stylistic
it affects caching, client expectations, and how CDNs handle your URLs.
2
Type hints are the single source of truth. FastAPI reads them to perform validation, type coercion, and OpenAPI documentation generation. Write accurate type hints and the rest follows automatically.
3
Default values determine requirement. An argument without a default is a required parameter
FastAPI will reject requests that omit it. An argument with a default is optional. This is the entire rule.
4
FastAPI supports multi-value query parameters natively. Define the parameter as list[str] and the client repeats the key
?tag=python&tag=web. No custom parsing required.
5
Annotated is the correct pattern for combining type hints with Query() or Path() metadata. It keeps the base type visible to IDEs, type checkers, and every tool in the Python ecosystem.
6
The :path converter is required for capturing path parameter values that contain forward slashes. Without it, matching stops at the first slash. This is opt-in by design
you must declare it explicitly.
7
422 errors contain structured detail arrays
parse them in your clients instead of treating them as opaque failures. The loc, msg, and type fields tell you exactly which parameter failed and why.
8
Regex constraints via Query(pattern=...) have CPU cost on hot paths. Anchor your patterns, test them against adversarial inputs, and measure before deploying on high-traffic endpoints. Prefer numeric and length constraints where they are sufficient.

Common mistakes to avoid

5 patterns
×

Using path parameters for filtering or pagination

Symptom
URLs become deeply nested and fragile — /items/active/sort/name/page/2 — and every filter combination becomes a unique URL path. Caching breaks because you cannot wildcard filter combinations in most proxy configurations. Adding a new filter requires a new route.
Fix
Path parameters belong to resource identity only: /items/{id}. Move all filtering, sorting, and pagination to query parameters: /items?status=active&sort=name&page=2. This is how HTTP was designed and how CDNs expect URLs to behave.
×

Assigning Query() or Path() as a default value instead of using Annotated

Symptom
IDE autocompletion shows the parameter type as Query or Path object rather than int or str. Type checkers like mypy and pyright report incorrect types. Developers reading the function signature cannot determine the actual type at a glance.
Fix
Use Annotated[int, Query(ge=1)] instead of param: int = Query(ge=1). The Annotated pattern keeps the base type in the first position — visible to every tool in the chain — while Query() or Path() carries the validation metadata separately. This is the pattern FastAPI's own documentation recommends and it is the only pattern that works cleanly with all major Python type checkers.
×

Not providing a default value for logically optional query parameters

Symptom
Clients receive 422 errors when they omit the parameter, even though the API is designed to function without it. This is a contract violation — the API promised the parameter was optional but requires it at runtime.
Fix
Assign a default: tag: Optional[str] = None or page: int = 1. A parameter without a default is a required parameter — FastAPI will enforce that. If the parameter is optional in your design, the default value is how you declare that intent.
×

Forgetting the :path converter for path parameters that may contain slashes

Symptom
A request to /files/documents/quarterly/report.pdf captures only documents as the file_path value. The remaining segments are silently dropped or cause a route mismatch. The bug is invisible in unit tests that use single-segment paths.
Fix
Add the :path converter to the route: @app.get('/files/{file_path:path}'). Test explicitly with multi-segment paths in your test suite — use paths with at least two slashes to confirm the converter is working.
×

Assuming query parameter names are case-insensitive

Symptom
Client sends ?Page=2 but the function argument is page: int. FastAPI does not find a matching argument for Page, treats the request as if page was not provided, and falls back to the default value. The client never receives an error — the page parameter is silently ignored.
Fix
Query parameter name matching is case-sensitive and exact. The URL key ?Page= does not match the function argument page. Document the expected casing in your OpenAPI spec and add a contract test that sends the exact key names your API expects. If you receive bug reports about ignored parameters, case sensitivity is the first thing to check.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does FastAPI's internal resolution logic determine if a function arg...
Q02SENIOR
Why is the Annotated pattern preferred over assigning Query() or Path() ...
Q03SENIOR
Explain the Path Converter concept. How would you capture a full file pa...
Q04SENIOR
Scenario: You have a query parameter tags that should accept a list of s...
Q05SENIOR
In a high-performance system, how does FastAPI's use of Pydantic for par...
Q01 of 05SENIOR

How does FastAPI's internal resolution logic determine if a function argument is a Body, Path, or Query parameter?

ANSWER
FastAPI resolves parameter categories at application startup — not at request time — which means the schema is built once and reused for every request. The resolution order is: (1) If the argument name appears in curly braces in the route path string, it is a Path parameter. (2) If the argument type is a Pydantic BaseModel subclass, or explicitly annotated with Body(), it is a request body parameter. (3) Simple scalar types — int, str, float, bool — that do not match a path placeholder become Query parameters automatically. (4) Special injection types like Request, Response, BackgroundTasks, and Depends() are handled by FastAPI's dependency system and are not bound to any HTTP parameter category. This resolution is purely structural — it reads your function signature and the route string. There is no runtime introspection per request. The implication is that renaming an argument without updating the route string silently changes a Path parameter into a Query parameter, which is a common source of subtle bugs.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between a path parameter and a query parameter?
02
How does FastAPI handle boolean query parameters?
03
Can a query parameter be a list?
04
What is the 422 Unprocessable Entity error in FastAPI?
🔥

That's Python Libraries. Mark it forged?

4 min read · try the examples if you haven't

Previous
NumPy where, select and piecewise — Conditional Array Operations
37 / 51 · Python Libraries
Next
FastAPI Request Body and Pydantic Models