Routing¶
A velociraptor does not blunder through the undergrowth hoping to trip over prey. It reads the terrain, picks its line, and strikes exactly where the path allows. VelociPy's router hunts with the same precision: a radix tree that maps every incoming path and method to a pre-built handler in a single, decisive traversal.
This page shows how to lay out routes, capture path parameters, group endpoints with APIRouter, match catch-all paths, and reverse named routes back into URLs.
Run the examples
See examples/routing.py for a runnable demo of sub-routers, typed path parameters, catch-all routes, and URL reversal.
Your first route¶
Create an app and attach a handler with the method decorator you need. The smallest hunt starts with a single GET:
from velocipy import VelociPy
app = VelociPy()
@app.get("/")
async def root():
return {"hello": "world"}
@app.post("/items")
async def create_item(item: dict):
return item
VelociPy supports the usual HTTP verbs: get, post, put, delete, patch, head, and options. Each decorator registers the endpoint under the given path and method. Return a dict or list and VelociPy serializes it as JSON automatically.
Async or sync handlers
Handlers can be async or plain functions. The framework resolves parameters and calls the endpoint with identical mechanics either way.
Path parameters¶
Path segments wrapped in {name} become parameters. Declare the parameter in the function signature and VelociPy passes the captured value straight through.
The router recognizes the parameter type from your annotation. Supported path converters include str, int, float, and bool. If a value cannot be converted, VelociPy returns a 422 Unprocessable Entity with the location set to path.
Explicit converters
You can also write the converter directly in the path: /{item_id:int}, /{price:float}, /{flag:bool}. This is useful when you want the URL itself to document the expected shape.
Multiple parameters¶
Add as many segments as the track demands:
@app.get("/users/{user_id:int}/orders/{order_id:int}")
async def get_order(user_id: int, order_id: int):
return {"user_id": user_id, "order_id": order_id}
The parameters arrive in the order they appear in the path, so the names in your handler do not have to match the route names - but matching them keeps your code readable.
Sub-routers¶
When the hunting ground grows, split the pack into smaller groups. APIRouter collects related endpoints under a common prefix and tags them for OpenAPI.
from velocipy import APIRouter, VelociPy
app = VelociPy()
users = APIRouter(prefix="/users", name="users", tags=["users"])
@users.get("/")
async def list_users():
return [{"id": 1, "name": "Alice"}]
@users.get("/{user_id:int}", name="get_user")
async def get_user(user_id: int):
return {"id": user_id}
app.include_router(users)
app.include_router(users) merges the router's prefix with each route path, applies the router-level tags, and registers the endpoints on the main application. The name argument on the router becomes a namespace for URL generation.
Include-time prefixes
You can add another prefix when including a router: app.include_router(users, prefix="/api/v1"). The final path becomes /api/v1/users/{user_id:int}.
Catch-all routes¶
Some paths cannot be known ahead of time: static file trees, proxy targets, documentation slugs. Use {name*} or {name:path*} to let a route consume the rest of the URL.
/static/css/main.css captures path="css/main.css". A catch-all is always the last segment of the route and greedily consumes everything after it.
Catch-all placement
Register catch-all routes after more specific routes. The router prefers static and dynamic segments first, but ordering still matters when multiple patterns could overlap.
Named routes and URL reversal¶
A route that knows only how to receive requests is half trained. Teach it a name and you can build URLs from code instead of hard-coding strings.
@app.get("/search", name="search")
async def search(q: str):
return {"q": q}
@app.get("/links")
async def links():
return {
"user": app.url_for("users.get_user", user_id=42),
"search": app.url_for("search", q="velocipy"),
}
url_for matches keyword arguments against the path parameters first; anything left over becomes query-string arguments. Named routes inside a router use the namespace prefix: "users.get_user".
Router namespace
If the router has no name, its routes are registered with their plain route names. If the router has a name, every named route inside it is prefixed: "router_name.route_name".
Route metadata¶
Route decorators accept extra arguments that shape OpenAPI documentation, status codes, response models, and deprecation flags. Use them to keep the generated docs accurate without touching the handler body.
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, tags=["items"])
async def create_item() -> dict:
return {"id": 1, "name": "New item"}
@app.get("/items/{item_id}", tags=["items"], summary="Get an item")
async def get_item(item_id: int):
return {"id": item_id}
@app.get("/legacy", deprecated=True)
async def legacy():
return {"deprecated": True}
@app.get("/text", response_class=PlainTextResponse)
async def text():
return "plain text response"
Common route arguments:
| Argument | Purpose |
|---|---|
status_code |
Default HTTP status for the response. |
response_model |
Model used to validate and serialize the response body. |
response_class |
Response class used to wrap the endpoint result. |
tags |
OpenAPI tags shown in the docs. |
summary |
Short OpenAPI summary. |
description |
Long OpenAPI description. |
operation_id |
Unique operation identifier for OpenAPI. |
deprecated |
Mark the operation as deprecated in OpenAPI. |
name |
Route name for app.url_for(...). |
openapi |
Explicit OpenAPI(...) metadata object. |
See examples/route_extras.py for a complete demo.
Method routing and custom verbs¶
For the rare case where one path must answer several verbs with different claws, use the generic @app.route decorator:
@app.route("/resource", methods={"GET", "POST", "PUT"})
async def resource():
return {"method": "varies"}
The verb-specific decorators are shortcuts for this call. If a request arrives with a method the path does not support, VelociPy returns 405 Method Not Allowed automatically.
At a glance¶
| Feature | Syntax | Best for |
|---|---|---|
| Static route | @app.get("/users") |
Fixed paths. |
| Typed path param | /{id:int}, /{price:float} |
IDs, slugs, numeric segments. |
| Plain path param | /{name} |
String segments with annotation coercion. |
| Catch-all | /{path*} or {path:path*} |
File serving, proxies, unknown depth. |
| Sub-router | APIRouter(prefix="/api") |
Grouping related endpoints. |
| Named route | name="get_user" |
URL reversal with app.url_for. |
| Router namespace | APIRouter(name="users") |
Avoiding name collisions across routers. |
| Route metadata | tags, summary, deprecated, etc. |
OpenAPI documentation. |
Related examples¶
| Example | What it shows |
|---|---|
examples/routing.py |
Sub-routers, typed path params, catch-all routes, url_for. |
examples/route_extras.py |
Status codes, response models, deprecation, custom response classes. |
examples/basic.py |
Routing with validation, dependencies, and OpenAPI. |