Skip to content

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.

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    return {"item_id": item_id}

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.

@app.get("/static/{path*}")
async def serve_static(path: str):
    return {"requested_path": path}

/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.

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.