FastAPI / Basic: HTTP and typed boundaries / Lesson 2
Path, query, and type contracts
Declare where values come from, how they are converted, and which bounds define a valid request.
On this page
Lesson goals
- Explain the assumptions and boundaries of path, query, and type contracts.
- Interpret both complete examples and justify their observed outputs.
- Diagnose the listed failures and apply the worked solutions while keeping identity, state, and lifecycle ownership explicit.
Make the source of each input visible
A path parameter participates in route identity; query parameters add request options without changing that path. FastAPI infers supported simple parameters by declaration, while explicit Path and Query metadata make constraints clearer. Use Annotated to keep a Python type and its request metadata together. A required parameter differs from one with a default, and a value that permits None is not automatically optional to omit without an appropriate default. Keep field names, aliases, repeated values, and client expectations documented. Do not pull arbitrary request dictionaries deep into business code when a narrow typed contract can be declared.
Separate parsing from bounds
Text from a URL must be interpreted before an integer or boolean can be used. Framework conversion is part of the declared contract, not a reason to assume strict rejection of every numeric string. Bounds such as ge and le reject values outside the permitted range after supported parsing. The first example accepts limit=3, applies a default when absent, and rejects zero. Validate the input shape and range before expensive work, while remembering that a maximum item count does not limit total request bytes, execution time, or concurrency. Those are separate operational controls.
from typing import Annotated
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/items")
def items(limit: Annotated[int, Query(ge=1, le=5)] = 2):
return {"limit": limit}
with TestClient(app) as client:
print(client.get("/items").json())
print(client.get("/items?limit=3").json())
print(client.get("/items?limit=0").status_code)
{'limit': 2}
{'limit': 3}
422
The default is two, three is valid, and zero fails with 422 before the handler returns a result. No external operation occurs.
Model repeated query values deliberately
A list query parameter needs an explicit query declaration so its source is unambiguous. Repeated tag values can become a list, while a single comma-separated string is not automatically the same protocol. The second example preserves two separate tag parameters and returns their order. Decide whether duplicates are accepted, normalized, or rejected according to the API contract. Avoid relying on one proxy or clientโs incidental treatment of repeated values, especially for security-sensitive decisions. If a parameter must occur once, enforce that condition explicitly rather than assuming the first or last value is universally chosen.
from typing import Annotated
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/search")
def search(tags: Annotated[list[str], Query()]):
return {"tags": tags}
with TestClient(app) as client:
response = client.get("/search", params=[("tags", "blue"), ("tags", "green")])
print(response.json())
print(client.get("/search").status_code)
{'tags': ['blue', 'green']}
422
Repeated keys become the declared list in order. With no default the list is required, so omission yields 422.
Use aliases without hiding meaning
An external parameter name can differ from its Python variable through an alias, but error locations and schema should still help a client correct the request. Version changes can affect alias behavior, so verify the pinned baseline and test the public spelling. Route parameters cannot be made optional simply by giving them a Python default; a different route shape is required. Prefer clear names and stable bounds over overly clever declarations. Validation establishes transport shape, not ownership of a resource, account permissions, or the truth of a business claim supplied by the caller.
Test defaults, boundaries, and wrong forms
A meaningful request test includes absent values, a valid minimum and maximum, out-of-range input, wrong type, repeated values, and the documented alias. Check stable status and relevant error locations rather than copying a complete error message that may include version-dependent detail. The examples use isolated TestClient applications and synthetic values only; they contact no service or database. Keep invalid requests from reaching side effects and record safe validation diagnostics without logging credentials or complete request data. A well-defined input contract is useful because clients can predict both accepted behavior and an actionable failure.
Common errors and fixes
A query list is interpreted as another input source
- Why it happens
- The list lacked an explicit Query declaration.
- How to fix it
- Declare the query source with the list type.
- Prevent it next time
- Test a real URL with repeated keys.
Optional type is assumed to mean omittable
- Why it happens
- Nullability and default presence were conflated.
- How to fix it
- Define both allowed values and omission behavior.
- Prevent it next time
- Test absent and explicit values separately.
A count limit is called a request-size limit
- Why it happens
- Field validation was confused with transport and resource budgets.
- How to fix it
- Apply separate byte, time, and concurrency controls.
- Prevent it next time
- Document the unit protected by each limit.
Check your understanding
1. A nullable query parameter has no default. What distinction should be tested?
Show worked solution
Whether omission is accepted is separate from whether a parsed value can be None. Define an explicit default when omission is intended and test the public request shape. Do not infer API requiredness from the word optional in a Python type discussion.
2. The API accepts tags=a&tags=b. Should tags=a,b automatically have the same result?
Show worked solution
No. Under the declared list contract the latter is one string containing a comma unless the application explicitly parses a different format. Document and test one protocol instead of silently combining incompatible client conventions.
Key takeaways
- A path parameter participates in route identity; query parameters add request options without changing that path.
- A list query parameter needs an explicit query declaration so its source is unambiguous.
- A meaningful request test includes absent values, a valid minimum and maximum, out-of-range input, wrong type, repeated values, and the documented alias.