Controllers¶
For anything beyond a one-off handler, grouping related request logic into a controller class keeps
routes tidy. A controller is a plain class with async action methods.
Basic controllers¶
# app/controllers/post_controller.py
from arvel.routing import Controller
class PostController(Controller):
async def index(self, request):
return await Post.paginate(per_page=20)
async def show(self, request, post: Post): # implicit model binding
return post
Point a route at an action:
from arvel import Route
Route.get("/posts", PostController().index, name="posts.index")
Route.get("/posts/{post}", PostController().show, name="posts.show") # {post} → implicit Post binding
This bound-method form constructs the controller once, at route-definition time — fine for a
controller with no constructor dependencies. When a controller needs per-request dependency
injection, bind it as a class instead (Route.resource(...) below, or a
single-action controller), so the container resolves a fresh instance
per request.
Single action controllers¶
If a controller action is particularly complex, it can be convenient to dedicate an entire class to
that single action. Give the class an __call__ method and bind the class directly to a route —
no Controller base, no action name:
class ShowDashboard:
def __init__(self, reports: ReportRepository) -> None: # constructor deps, container-resolved
self.reports = reports
async def __call__(self, request, team_id: str) -> dict:
return {"reports": await self.reports.for_team(team_id)}
Route.get("/teams/{team_id}/dashboard", ShowDashboard)
Pass the class, not an instance — the kernel instantiates it via the container per request (the same
app.make(...) path middleware classes go through), so constructor dependencies resolve normally.
Controller middleware¶
Attach middleware to a resource controller's actions by overriding middleware() — return
ControllerMiddleware entries (each scopable to specific actions with only/except_):
from arvel.routing import ControllerMiddleware
class PostController(Controller):
@classmethod
def middleware(cls):
return [
ControllerMiddleware("auth"), # every action
ControllerMiddleware("throttle:api", only=("store", "update")),
]
middleware() is consulted by Route.resource/api_resource; for a plain bound-method route,
attach middleware on the route itself (see Middleware).
Resource controllers¶
A resource controller handles the standard CRUD verbs for an entity. Route.resource wires all of
them to a controller in one call — binding only the actions the controller actually implements:
| Verb & URI | Action | Route name |
|---|---|---|
GET /posts |
index |
posts.index |
GET /posts/create |
create |
posts.create |
POST /posts |
store |
posts.store |
GET /posts/{post} |
show |
posts.show |
GET /posts/{post}/edit |
edit |
posts.edit |
PUT/PATCH /posts/{post} |
update |
posts.update |
DELETE /posts/{post} |
destroy |
posts.destroy |
The path segment is the singularized resource name (posts → {post}), which is exactly the
name implicit model binding keys on — so show(self, request, post: Post) receives
the loaded model, not a raw id.
For a JSON API, drop the HTML-form actions (create/edit) with api=True, and narrow the set
with only/except_. These are alternatives — pick the one call that fits, don't stack them:
Route.resource("posts", PostController, api=True) # …or
Route.resource("posts", PostController, only=["index", "show"]) # …or
Route.resource("posts", PostController, except_=["destroy"])
Authorizing resource actions¶
Bind a policy to a resource controller so each action is authorization-checked against its route-bound model (see Authorization). The declarative form is preferred:
class PostController(Controller):
__resource_policy__ = Post # index/show/... checked against PostPolicy
(PostController.authorize_resource(Post) after the class body is the equivalent imperative form.)
Each action maps to an ability, checked against either the model class or the route-bound instance:
| Action | Ability | Authorized against |
|---|---|---|
index |
viewAny |
the model class |
create / store |
create |
the model class |
show |
view |
the bound instance |
edit / update |
update |
the bound instance |
destroy |
delete |
the bound instance |
Instance actions authorize against the route-bound model (pair with route–model binding);
a denied check raises 403 before the action body runs.