On this page

On this page

Controllers

Introduction

Instead of defining all of your request handling as closures in the route file, you may organize this behavior into controller classes. Controllers live in app/Http/Controllers.

Writing controllers

Basic controllers

A controller is a class. Each method is an action. The method receives the request and returns a response, a string, a view, or a JSON value.

app/Http/Controllers/UserController.ts
import type { Request } from "@bunyad/http";
import { json } from "@bunyad/http";

export default class UserController {
  index(request: Request) {
    return json({ q: request.string("q", "") });
  }
}

Point the route at the class and the method:

import UserController from "@/Http/Controllers/UserController.ts";

Route.get("/users", [UserController, "index"]);

Generate the file with:

bunyad make:controller UserController

The kernel keeps one instance of the controller for the process and calls the method on each request.

Returning a Response sends that response as-is. A string is HTML. A plain object or a model is JSON. Values with toJSON are serialized.

Single action controllers

When a controller does one thing, give it an index method and a single route. Bunyad does not invoke a class just because it has a method named a special way. The route tuple always names the method: [ReportController, "index"].

Controller middleware

Assign middleware on the route that points at the controller, or on the group that contains that route. The controller method does not declare the middleware itself.

Route.middleware("web").group(() => {
  Route.middleware(auth()).group(() => {
    Route.get("/dashboard", [AuthController, "dashboard"]);
  });
});

The web group starts the session. auth() then sees that session. Reverse the order and the authentication check runs before the session exists.

Resource controllers

A resource controller handles the usual actions for a noun: list, create, store, show, edit, update, and destroy. Route.resource registers those routes for you.

Route.resource("photos", PhotoController);
Verb URI Action Name
GET /photos index photos.index
GET /photos/create create photos.create
POST /photos store photos.store
GET /photos/{photo} show photos.show
GET /photos/{photo}/edit edit photos.edit
PUT/PATCH /photos/{photo} update photos.update
DELETE /photos/{photo} destroy photos.destroy

Route.apiResource omits create and edit, which exist to return HTML forms.

Route.apiResource("photos", PhotoController);

Singleton resources

A singleton has one instance (no {id} segment). Default actions are show, edit, and update. Chain creatable() / destroyable(), or pass those flags in options:

Route.singleton("profile", ProfileController).creatable().destroyable();

Route.apiSingleton("profile", ProfileController, { creatable: true });
Verb URI Action
GET /profile show
GET /profile/edit edit
PUT/PATCH /profile update
GET /profile/create create (creatable)
POST /profile store (creatable)
DELETE /profile destroy (destroyable)

apiSingleton omits create and edit.

Partial resource routes

only and except limit the actions.

Route.resource("photos", PhotoController, { only: ["index", "show"] });
Route.resource("photos", PhotoController, { except: ["destroy"] });

Nested resources

Dot the name when the child belongs to a parent. The URI includes both parameters.

Route.resource("photos.comments", PhotoCommentController);

shallow: true drops the parent segment from the member routes (show, edit, update, destroy) and keeps it on the collection routes (index, create, store).

Route.resource("photos.comments", PhotoCommentController, { shallow: true });

Naming resource routes

names replaces the generated names. Pass a string to prefix every name, or an object to rename individual actions.

Route.resource("photos", PhotoController, { names: "admin.photos" });

Naming resource route parameters

parameters renames the URI parameter. The default is the singular of the last segment, so photos uses {photo}.

Route.resource("users", AdminUserController, {
  parameters: { users: "admin_user" },
});

The show route is then /users/{admin_user}.

Scoping resource routes

Chain scopeBindings() when a nested child must belong to the parent resolved for that request. The parent model implements resolveChildRouteBinding. See routing.

Route.resource("photos.comments", PhotoCommentController).scopeBindings();

Middleware and resource controllers

Middleware on the group applies to every action the resource registered.

Route.middleware("web").group(() => {
  Route.resource("photos", PhotoController);
});

Dependency injection and controllers

Type the action’s first parameter as Request and the kernel passes the current request. A form request is a Request that also authorizes and validates. Call from so that work finishes before you read the payload:

import LoginRequest from "@/Http/Requests/LoginRequest.ts";

export default class AuthController {
  async login(request: Request) {
    const form = await LoginRequest.from(request);
    return form.validated();
  }
}

If authorize returns false, the response is 403. If validation fails, an HTML client is redirected back and a JSON client receives 422. Requests covers the input API. app/Http/Requests/Auth/LoginRequest.ts in the Views starter is a complete example: it validates, checks the credentials, and locks out repeated failures.