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
- Send rich responses with the Request & Response API.
- Add cross-cutting behavior with middleware.