Files

136 lines
4.4 KiB
ReStructuredText

Composing Apps with Routers
===========================
As an application grows past a single file, you'll want to declare routes
where the code lives — a ``users`` module owns the user routes, a
``billing`` module owns billing — and assemble everything in one place.
If you've used Flask Blueprints or FastAPI's ``APIRouter``, this is the
same idea: :class:`responder.Router` records route declarations *without*
an ``API`` instance, and ``api.include_router()`` attaches them later.
This avoids the two classic failure modes of single-``API`` apps: stuffing
every route into one file, or importing the ``api`` object into every
module (and the circular imports that follow).
Declaring Routes in a Module
----------------------------
A ``Router`` supports the same decorators as the ``API````route()``,
the verb shortcuts (``get``, ``post``, ``put``, ``patch``, ``delete``,
``head``, ``options``), ``websocket_route()``, and ``before_request`` — but
only records the declarations::
# users.py
from responder import Router
router = Router(prefix="/users", tags=["users"])
@router.get("")
def list_users(req, resp):
resp.media = []
@router.get("/{user_id:int}")
def get_user(req, resp, *, user_id):
resp.media = {"id": user_id}
Nothing runs yet — there is no application here to run. The main module
assembles the app::
# app.py
import responder
import billing
import users
api = responder.API()
api.include_router(users.router, prefix="/v1") # /v1/users, /v1/users/{id}
api.include_router(billing.router, prefix="/v1")
Each recorded route is replayed through ``api.route()``, so everything
works exactly as if it had been declared on the API directly: auth
inheritance, ``Depends`` guards, Pydantic models, and OpenAPI metadata.
Inclusion is a *snapshot*: routes declared on a router after it has been
included are not picked up by that earlier inclusion.
Nesting Routers
---------------
Routers include other routers, and prefixes compose::
api_v1 = Router(prefix="/v1")
api_v1.include_router(users.router) # /v1/users/...
api_v1.include_router(admin.router, prefix="/admin")
api.include_router(api_v1)
Along the way, ``tags`` merge (outermost first, duplicates dropped) and
``dependencies`` concatenate (outermost guards run first).
Group Defaults: Tags, Dependencies, Auth
----------------------------------------
Group-level values apply to every route in the router — and can still be
overridden per route::
from responder import Depends, Router
from responder.ext.auth import BearerAuth
def require_staff(req):
...
admin = Router(
prefix="/admin",
tags=["admin"],
dependencies=[Depends(require_staff)],
auth=BearerAuth(verify=lookup_token),
)
@admin.get("/stats")
def stats(req, resp, *, user):
resp.media = {"user": user}
@admin.get("/health", auth=None) # opt this one route out of auth
def health(req, resp):
resp.media = {"ok": True}
Auth resolution picks the most specific explicit setting: a route's own
``auth=`` wins over the router's, which wins over
``include_router(..., auth=...)``, which wins over the app-level
``API(auth=...)`` default.
Before-request hooks declared on a router only run for request paths under
the prefix the router was mounted at (like ``api.group()`` hooks)::
@admin.before_request()
def audit(req, resp):
log.info("admin request", path=req.url.path)
Including a Router Twice
------------------------
The same router can be included at several prefixes — handy for serving an
API under a legacy path during a migration::
api.include_router(users.router, prefix="/v1")
api.include_router(users.router, prefix="/api/v1")
One caveat: route metadata (auth, tags, dependencies) is attached to the
view function itself, so including the same *view* twice with different
effective metadata raises a ``ValueError`` rather than silently rewriting
the earlier inclusion. Include with identical settings, or use separate
routers with separate view functions.
Routers vs. ``api.group()``
---------------------------
``api.group(prefix)`` remains for quick, same-file prefix grouping — it
registers routes immediately on the live API. Reach for ``Router`` when
routes live in their own modules, when you need group-level ``tags`` /
``dependencies`` / ``auth``, or when groups need to nest.