Routers¶
A router holds routes declared in one module, to be mounted into an app in another.
# users.py
from oxbrook import Request, Router
router = Router(prefix="/users")
@router.get("")
async def list_users(_: Request): ...
@router.get("/{user_id}")
async def get_user(_: Request, user_id: int): ...
@router.patch("/{user_id}")
async def update_user(_: Request, user_id: int): ...
# main.py
from oxbrook import App
import users
app = App()
app.include(users.router, prefix="/api/v1")
# GET /api/v1/users, GET /api/v1/users/{user_id}, PATCH /api/v1/users/{user_id}
A router has the same decorators as an app — get, post, put, patch,
delete, route, websocket — and tool=True works the same way.
Prefixes¶
Prefixes and paths join literally. A prefix starts with / and does not end
with one.
| router prefix | decorator | served at |
|---|---|---|
/users |
@router.get("") |
/users |
/users |
@router.get("/") |
/users/ |
/users |
@router.get("/{user_id}") |
/users/{user_id} |
The first two are different routes. A trailing slash is significant everywhere in Oxbrook, and a router does not change that.
A prefix can declare path parameters. The handler accepts them like any other:
tenant = Router(prefix="/tenants/{tenant}")
@tenant.get("/invoices")
async def invoices(_: Request, tenant: str): ...
Nesting¶
Routers include routers. Prefixes accumulate.
admin = Router(prefix="/admin")
@admin.get("/stats")
async def stats(_: Request): ...
users.include(admin) # /users/admin/stats
app.include(users, prefix="/api") # /api/users/admin/stats
Middleware¶
Middleware registered on a router runs only for that router's routes, and for routers included into it. It runs inside the app's middleware.
@admin.middleware
async def require_admin(request, call_next):
if not is_admin(request):
raise HTTPError(403)
return await call_next(request)
Order, outermost first: app middleware, then each router's middleware from the
outermost router inwards, then the handler. An HTTPError raised by an inner
router's middleware reaches the app's middleware as a reply with its status, not
as an exception, so an access log records the status the client received.
When mistakes are caught¶
A handler is checked when it is decorated, against the router's own path, and
again when the router is included, against the full path. A conflict between
two routers — the same method and the same route shape — raises at the second
include.
A router cannot change after it has been included. Its routes were copied into
the app at that moment, so a route added afterwards would never be served; adding
one raises RuntimeError instead. Include a router after everything is declared
on it.
Routers cost nothing per request: they are flattened into the app's route table
at include.
Reaching the app from a router module¶
A module that declares a router usually cannot import the app without an import cycle. Reach it through the request instead:
@router.get("/feed")
async def feed(request: Request):
return SSE(request.app.topic("orders").subscribe())
@router.get("/users")
async def users(request: Request):
return await request.state.db.fetch("select ...")
request.state holds what the app's lifespans yielded. See
Lifespan.