Requests & responses¶
A hunt succeeds because the raptor reads every sign: the scent on the wind, the snap of a twig, the shape of the terrain. VelociPy parses HTTP with the same attentiveness - query strings, headers, cookies, path segments, and body content become typed parameters you declare once and receive already parsed.
This page walks through reading request data, validating it, and returning the response shape that fits the moment.
Run the examples
See examples/query_params.py, examples/headers_cookies.py, and examples/responses.py for runnable demos of everything covered here.
Reading request data¶
VelociPy injects request data through Python type hints. The framework looks at each parameter and decides where it comes from:
- Path parameters come from the route.
Query,Header,Cookie, andFormmarkers come from their matching request locations.- Pydantic or msgspec models come from the JSON body.
dictorlistparameters come from the raw JSON body.Requestparameters receive the full request object.
from typing import Annotated
from velocipy import Cookie, Header, Query, VelociPy
app = VelociPy()
@app.get("/search")
async def search(
q: Annotated[str, Query()],
x_request_id: Annotated[str | None, Header()] = None,
session: Annotated[str | None, Cookie()] = None,
):
return {"q": q, "x_request_id": x_request_id, "session": session}
The Annotated[..., Marker()] form is recommended because it keeps the type and source together. The framework coerces values to the declared type and returns 422 Unprocessable Entity when the data does not fit.
Without Annotated
For simple cases you can also write q: str = Query(...) or q: str = Query("default"). Annotated is preferred when you need aliases, descriptions, or stricter type-checking.
Query parameters¶
Query parameters are the easiest trail to follow. Declare them with Query(...) to make them required, or give them a default to make them optional.
from typing import Annotated, Any
from velocipy import Query, VelociPy
app = VelociPy()
@app.get("/items")
async def list_items(
q: Annotated[str, Query(...)],
limit: Annotated[int, Query(10)],
offset: int = 0,
) -> dict[str, Any]:
return {"q": q, "limit": limit, "offset": offset}
A request to /items?q=hoodie&limit=5 returns {"q": "hoodie", "limit": 5, "offset": 0}. Missing required parameters produce a 422 response.
Lists and aliases¶
Use list[T] or set[T] to accept repeated values, and alias to map a Python name to a different query key:
@app.get("/tags")
async def tags(tag: Annotated[list[str], Query(...)]) -> dict[str, list[str]]:
return {"tags": tag}
@app.get("/search")
async def search(
sort_by: Annotated[str, Query(..., alias="sort-by")]
) -> dict[str, str]:
return {"sort_by": sort_by}
/tags?tag=red&tag=blue captures {"tags": ["red", "blue"]} and /search?sort-by=price binds sort_by="price".
Supported scalar types
Query, header, cookie, and form parameters coerce to str, int, float, and bool. Booleans accept true/false, 1/0, yes/no, on/off.
See examples/query_params.py for more.
Headers and cookies¶
Headers and cookies work exactly like query parameters, but they read from their own parts of the request.
from typing import Annotated, Any
from velocipy import Cookie, Header, VelociPy
app = VelociPy()
@app.get("/items")
async def list_items(
x_token: Annotated[str, Header(...)],
user_agent: Annotated[str | None, Header(None)] = None,
) -> dict[str, Any]:
return {"token": x_token, "user_agent": user_agent}
@app.get("/theme")
async def theme(
session_theme: Annotated[str, Cookie(..., alias="theme")]
) -> dict[str, Any]:
return {"theme": session_theme}
Header names are case-insensitive and underscores in parameter names are converted to hyphens automatically, so user_agent matches the User-Agent header. Use alias to override this when the real header name is unusual.
Required cookies
A required Cookie(...) parameter raises 422 if the cookie is missing. For optional cookies, use Cookie(None) or Annotated[str | None, Cookie()] = None.
See examples/headers_cookies.py for more.
Body models¶
When the prey arrives as JSON, declare a model and VelociPy validates the body before the handler runs.
from pydantic import BaseModel
from velocipy import VelociPy
app = VelociPy()
class Item(BaseModel):
name: str
price: float
@app.post("/items")
async def create_item(item: Item):
return {"created": item}
You can use pydantic.BaseModel, msgspec.Struct, or any model type registered with the app's serialization backend. Invalid bodies return 422 with the validation errors.
Response models too
Use response_model=Item on the route decorator to validate and serialize the response through the same model. See Route metadata for details.
See examples/model_backends.py for msgspec, pydantic, and custom adapters.
Raw JSON bodies¶
For quick endpoints or internal tooling, accept a raw dict or list and skip the model layer:
VelociPy parses the JSON body and passes the result through. This is the lightest path, but you lose validation and OpenAPI schema detail.
Accessing the request object directly¶
Sometimes you need the full request: the URL, method, headers, stream, or form data. Declare a Request parameter and VelociPy hands it over.
from velocipy import Request, VelociPy
app = VelociPy()
@app.get("/info")
async def info(request: Request):
return {
"method": request.method,
"path": request.path,
"headers": request.headers,
}
The Request object is protocol-neutral, so the same code runs under ASGI and RSGI. Useful properties include:
| Property | Returns |
|---|---|
request.url |
Full request URL. |
request.method |
Uppercase HTTP method. |
request.path |
URL path. |
request.query_string |
Raw query string. |
request.headers |
Lowercase header names to values. |
request.query_params |
Parsed multi-value query mapping. |
request.cookies |
Parsed cookie mapping. |
request.path_params |
Route path parameters. |
For streamed or large bodies, call request.stream() as an async iterator instead of await request.body().
Payload size limits
Set max_content_length on the app to reject oversized bodies with 413 Payload Too Large. See examples/max_body_size.py.
Response types¶
A handler can return almost anything and VelociPy will wrap it sensibly:
dict/list→JSONResponsestr→PlainTextResponsebytes→Responsewithapplication/octet-streamNone→204 No ContentResponsesubclass → used as-is
When you need explicit control, return a response object directly.
Common response classes¶
from velocipy import VelociPy
from velocipy.http.responses import (
HTMLResponse,
PlainTextResponse,
RedirectResponse,
)
app = VelociPy()
@app.get("/text")
async def text():
return PlainTextResponse("hello")
@app.get("/html")
async def html():
return HTMLResponse("<h1>hello</h1>")
@app.get("/redirect")
async def redirect():
return RedirectResponse("/text")
Streaming responses¶
For responses that are too large to build in memory, stream byte chunks:
from collections.abc import AsyncIterator
from velocipy import VelociPy
from velocipy.http.responses import StreamingResponse
app = VelociPy()
async def byte_generator() -> AsyncIterator[bytes]:
for i in range(5):
yield f"chunk {i}\n".encode()
@app.get("/stream")
async def stream():
return StreamingResponse(byte_generator(), media_type="text/plain")
File responses¶
Serve files from disk with FileResponse. The framework handles content delivery through the active protocol adapter:
from velocipy import FileResponse, VelociPy
app = VelociPy()
@app.get("/report.pdf")
async def report():
return FileResponse("static/report.pdf")
See examples/responses.py for the full set.
Response metadata¶
Route decorators let you set status codes, response models, and response classes so the handler stays focused on business logic.
from velocipy import VelociPy
from velocipy.http.responses import PlainTextResponse
from velocipy.status import HTTP_201_CREATED
app = VelociPy()
@app.post("/items", status_code=HTTP_201_CREATED)
async def create_item():
return {"id": 1}
@app.get("/text", response_class=PlainTextResponse)
async def text():
return "plain text"
| Argument | Effect |
|---|---|
status_code |
Sets the response status. |
response_model |
Validates and serializes the returned value. |
response_class |
Wraps the returned value in a custom response class. |
At a glance¶
| Request source | Declaration | Coerced type |
|---|---|---|
| Path segment | item_id: int |
From route annotation or {name:int}. |
| Query string | q: Annotated[str, Query(...)] |
str, int, float, bool, list[T], set[T]. |
| Header | x_token: Annotated[str, Header(...)] |
str, int, float, bool. |
| Cookie | session: Annotated[str, Cookie(...)] |
str, int, float, bool. |
| JSON body model | item: Item where Item is a pydantic/msgspec model |
Validated model instance. |
| Raw JSON body | payload: dict |
Parsed JSON object/array. |
| Full request | request: Request |
Protocol-neutral request object. |
| Response shape | Result |
|---|---|
dict / list |
JSONResponse |
str |
PlainTextResponse |
bytes |
Response(media_type="application/octet-stream") |
None |
204 No Content |
Response subclass |
Used unchanged |
JSONResponse, HTMLResponse, PlainTextResponse, etc. |
Explicit response class |
Related examples¶
| Example | What it shows |
|---|---|
examples/query_params.py |
Required, default, list, and aliased query parameters. |
examples/headers_cookies.py |
Header and cookie injection. |
examples/responses.py |
Plain text, HTML, redirect, and streaming responses. |
examples/model_backends.py |
msgspec, pydantic, and custom model validation. |
examples/uploads.py |
UploadFile and multipart form handling. |
examples/form_parameters.py |
Form(...) field injection and form models. |