2. Path & query parameters¶
Our Tasks API needs to look things up by id and filter lists. Both come from
the URL: the path (/tasks/3) and the query string (/tasks?limit=10).
Veloce binds them to handler arguments by name and type.
Replace the body of tasks.py with:
from veloce import Veloce
app = Veloce(title="Tasks API", version="1.0.0")
# A throwaway list so the routes return something. We replace it with a real
# store two steps from now.
_demo_tasks = ["buy milk", "write tutorial", "ship release"]
@app.get("/")
async def index():
return {"message": "Tasks API is running"}
@app.get("/tasks/{task_id}")
async def get_task(task_id: int):
return {"id": task_id, "title": _demo_tasks[task_id % len(_demo_tasks)]}
@app.get("/tasks")
async def list_tasks(limit: int = 10, q: str = ""):
items = [t for t in _demo_tasks if q in t]
return {"tasks": items[:limit], "limit": limit, "q": q}
if __name__ == "__main__":
app.run(port=8000)
Run it and try:
- http://localhost:8000/tasks/2 →
{"id": 2, "title": "ship release"} - http://localhost:8000/tasks?limit=2 → the first two tasks
- http://localhost:8000/tasks?q=write → only matching tasks
Path parameters¶
{task_id} in the route declares a path parameter. The task_id: int
annotation tells Veloce to coerce the URL segment to an integer before your
handler runs. Request /tasks/abc and you get a 422 response — "abc" is
not an int — without writing any validation yourself.
404 vs 422 — annotation coerces, converter matches
Where the type lives changes the failure code. A handler annotation
(task_id: int) coerces after the route matches, so a bad value is a
422. An inline converter (/tasks/{task_id:int}) is part of matching,
so a non-integer segment never selects the route — /tasks/abc then becomes
a 404. FastAPI returns 422 in both cases; Veloce distinguishes them.
Use the annotation form when you want a validation error, the converter form
when a wrong type should not match the route at all.
Static routes always win
If you later add /tasks/recent alongside /tasks/{task_id}, the literal
recent route always matches first — Veloce's radix tree tries exact
segments before parameters, so registration order does not matter. You never
have to register specific routes before general ones.
Query parameters¶
limit and q are not in the path, so Veloce reads them from the query
string. The rule is: an argument whose name matches a path placeholder is a
path parameter; otherwise it is read from the query string. Giving each a
default (limit: int = 10, q: str = "") makes it optional, so /tasks works
with no query at all.
Query values are coerced too: ?limit=5 arrives as the integer 5, not the
string "5".
Adding constraints¶
When you need validation — a minimum page, a maximum length — use the
Query and Path marker
classes as the default:
from veloce import Path, Query, Veloce
app = Veloce(title="Tasks API", version="1.0.0")
_demo_tasks = ["buy milk", "write tutorial", "ship release"]
@app.get("/tasks/{task_id}")
async def get_task(task_id: int = Path(ge=0, description="Task id")):
return {"id": task_id, "title": _demo_tasks[task_id % len(_demo_tasks)]}
@app.get("/tasks")
async def list_tasks(limit: int = Query(default=10, ge=1, le=100), q: str = ""):
items = [t for t in _demo_tasks if q in t]
return {"tasks": items[:limit], "limit": limit, "q": q}
Now /tasks?limit=0 is rejected with a 422 because of ge=1, and the
constraints show up in the OpenAPI schema. See the
Parameters guide for the full set of markers
(Header, Cookie, Form, File) and constraints.
Next steps¶
Reading the URL is enough for lookups, but creating a task needs a request body. Next: Request bodies.