Skip to content

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:

Route.resource("posts", PostController)
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.

Generating controllers

arvel make:controller PostController          # a plain controller
arvel make:controller PostController -r       # a resource controller (stubbed actions)
arvel make:controller PostController --api    # a resourceful API controller