Skip to the content.

Routing

Routes map URLs to handler functions. axion uses regex-based path matching with type-safe parameter extraction, and keeps a router’s route collection ordered so that the first registered match wins.

Defining routes

Decorate a handler with @app.route:

from axion import WebServer
from axion.http import Response

app = WebServer(debug=True)


@app.route("/")
def index(response: Response):
    response.send("Hello, world!")

Routes answer GET by default. Specify methods to change that:

@app.route("/api/users", methods=["GET", "POST"])
def users(response: Response):
    ...

Path parameters

Use <name> placeholders. The matched value is passed to the handler as a string and is URL-decoded:

@app.route("/users/<username>")
def user(username: str, response: Response):
    response.json({"username": username})

Use <int:name> to match digits only. If the handler annotates the parameter as int, the value is coerced automatically:

@app.route("/api/items/<int:item_id>")
def item(item_id: int, response: Response):
    response.json({"id": item_id, "type": type(item_id).__name__})

/api/items/42 matches with item_id = 42; /api/items/abc does not match (and yields a 404). Parameters are available to the handler both as keyword arguments and through request.user_parameters.

First match wins

Routes are matched in registration order. The first route whose pattern matches the request path is used, so register more specific routes first:

@app.route("/users/me")       # specific route first
def me(response: Response):
    ...

@app.route("/users/<username>")   # fallback
def user(username: str, response: Response):
    ...

Async route handlers

Handlers may be async def coroutines — FastAPI-style. axion runs each async handler in its own event loop created for the request, so await works without sharing state across requests:

import asyncio

from axion import WebServer
from axion.http import Response

app = WebServer(debug=True)


@app.route("/api/status")
async def status(response: Response):
    await asyncio.sleep(0.01)
    response.json({"status": "ok"})

Async handlers support everything sync handlers do: path parameters, Pydantic body validation, dependency injection, and serializers. @app.error_route handlers can be async too. Sync handlers are unaffected; you can mix both freely.

Async middleware is not supported yet — the middleware chain is synchronous. app.use(async_mw) raises TypeError.

Not found and method errors

When no route matches, axion returns 404. When a path matches but the method is not allowed, it returns 405. Both have built-in HTML templates that you can override globally:

@app.error_route(404)
def not_found(request: Request, response: Response):
    response.send("That page does not exist.", 404)

error_route is a per-app convenience that forwards to the router’s add_error_handler(status_code, handler).

Modular routers

Use a standalone Router for reusable groups of routes, then mount it with a prefix:

# api.py
from axion import Router
from axion.http import Response

api_router = Router()


@api_router.route("/health")
def health(response: Response):
    response.json({"status": "ok"})
# main.py
from axion import WebServer

from api import api_router

app = WebServer(debug=True)
app.include_router(api_router, prefix="/api")

/api/health now serves the health check. Mounting also rewrites any path-scoped middleware registered on the router.

The Router class offers the full routing API for programmatic use:

Method Description
route(path, methods, middleware) Decorator form of add_route
add_route(path, handler, methods, middleware) Register a handler directly
use(path, middleware, methods) Register path-scoped middleware
add_error_handler(status_code, handler) Register an error handler
resolve(path, method) Match a path/method to a route
get_error_handler(status_code) Look up a registered error handler
routes() Registered routes, in order
path_middleware() Path-scoped middleware, in order
error_handlers() Registered error handlers

Custom error handler registrations on a router

When using a standalone router, register error handlers on it so they are carried over when the router is mounted:

api_router.add_error_handler(404, not_found)

Next steps