The basics
Middleware
Middleware inspect and filter the requests that reach your app: they can check a request, turn it away, or change the response on its way out.
Oriel's own middleware check that users are signed in, start the session and verify CSRF tokens. Yours live in
app/.
Defining middleware#
make:middleware writes a new middleware class in app/:
node oriel make:middleware EnsureTokenIsValidA middleware has a handle() method that receives the request and next, the rest of the app. This one lets the
request through only when its token input matches, and sends everyone else to /home:
import { redirect } from '@orieljs/framework'import type { HttpRequest, Middleware, Next } from '@orieljs/framework/contracts'export class EnsureTokenIsValid implements Middleware { async handle(request: HttpRequest, next: Next): Promise<Response> { if (request.input('token') !== 'my-secret-token') { return redirect('/home').toResponse(request) } return next(request) }}To let the request pass, call next(request) and return what it returns. Think of middleware as layers a request
passes through on its way to your route: each layer can examine the request and turn it away.
handle() returns a Response, so turn a redirect into one with its toResponse(request), or throw: abort(403)
ends the request with a 403.
Middleware and responses#
A middleware can do its work before the request goes on:
import type { HttpRequest, Middleware, Next } from '@orieljs/framework/contracts'export class BeforeMiddleware implements Middleware { async handle(request: HttpRequest, next: Next): Promise<Response> { // Perform action return next(request) }}or after the app has answered, with the response in hand:
import { Log } from '@orieljs/framework'import type { HttpRequest, Middleware, Next } from '@orieljs/framework/contracts'export class AfterMiddleware implements Middleware { async handle(request: HttpRequest, next: Next): Promise<Response> { const response = await next(request) Log.info('Answered', { path: request.path(), status: response.status }) return response }}Dependency injection#
A new instance of your middleware handles each request, built by the service container. Take the
services it needs with inject() in a field:
import { inject, redirect } from '@orieljs/framework'import { ConfigRepository, type HttpRequest, type Middleware, type Next } from '@orieljs/framework/contracts'export class EnsureTokenIsValid implements Middleware { readonly #config = inject(ConfigRepository) async handle(request: HttpRequest, next: Next): Promise<Response> { if (request.input('token') !== this.#config.get('services.webhooks.token')) { return redirect('/home').toResponse(request) } return next(request) }}Registering middleware#
Global middleware#
A global middleware runs on every request. Add it in bootstrap/, with withMiddleware():
Application.configure({ basePath: '.' }).withMiddleware((middleware) => { middleware.append(EnsureTokenIsValid)})append() adds it at the end of the global list, and prepend() at the start. The list starts with TrimStrings,
which trims every text input except password, password_confirmation and current_password, and
ConvertEmptyStringsToNull, which turns empty inputs into null.
Laravel's global list also checks trusted proxies and the size of the body. In Oriel these aren't middleware: the
request applies them from config/, as Requests explains.
Assigning middleware to routes#
Give a route a middleware with middleware(), by class or by alias:
Route.get('/profile', () => 'Profile').middleware(EnsureTokenIsValid)Route.get('/', () => 'Home').middleware(['auth', EnsureTokenIsValid])A group gives its middleware to every route in it: see route groups.
Excluding middleware#
withoutMiddleware() takes a middleware off one route of a group, whatever parameters it was given there:
Route.middleware(EnsureTokenIsValid).group(() => { Route.get('/', () => 'Home') Route.get('/profile', () => 'Profile').withoutMiddleware(EnsureTokenIsValid)})It removes a middleware a route gets from its group or from the web group, but not a global one. There is no
withoutMiddleware() for a whole group: put those routes in a group of their own.
Middleware groups#
A group puts several middleware under one name, which routes use as they would an alias. Define one with group():
Application.configure({ basePath: '.' }).withMiddleware((middleware) => { middleware.group('group-name', [First, Second])})Route.get('/', () => 'Home').middleware('group-name')Route.middleware('group-name').group(() => { // ...})A group's list may name other groups, and aliases with their parameters: ['auth', 'throttle:60,1'].
The default middleware groups#
The routes in routes/ are in the web group, and those in routes/ in the api group:
The web group | What it does |
|---|---|
EncryptCookies | Decrypts the request's cookies and encrypts the response's |
AddQueuedCookiesToResponse | Adds the cookies queued on the cookie jar (Cookie.queue()) to the response |
StartSession | Loads the session, and saves it once the response is ready |
VerifyCsrfToken | Refuses a form without the CSRF token (see CSRF protection) |
The api group is empty: API routes have no session, no cookie encryption and no CSRF check.
web() and api() change the default groups: append adds middleware at the end, prepend at the start, and
remove takes them out. The Inertia starter kits add two middleware of their own to the web group, in
bootstrap/:
Application.configure({ basePath: '.' }).withMiddleware((middleware) => { middleware.web({ append: [HandleAppearance, HandleInertiaRequests] })})group('web', [...]) replaces the web group with the list you give it.
Middleware aliases#
An alias is a short name for a middleware class, which routes can use with parameters. Define yours with alias():
Application.configure({ basePath: '.' }).withMiddleware((middleware) => { middleware.alias({ subscribed: EnsureUserIsSubscribed })})Route.get('/profile', () => 'Profile').middleware('subscribed')A name that is neither an alias nor a group fails when a route that uses it is called, with a 500 that starts "Middleware [subscribed] is not defined." These aliases are defined for you. Authentication, Authorization, Email verification and History encryption cover the ones they own.
| Alias | Middleware | What it does |
|---|---|---|
auth | Authenticate | Lets signed-in users through. A guest gets a 401 for JSON, or a redirect to the login page. auth:token names the guard; with several, the first that has a user wins |
can | Authorize | Checks an ability with the gate, as can:update,post; a parameter that names a route parameter passes the bound model. A denial is a 403 |
guest | RedirectIfAuthenticated | Lets guests through, and redirects signed-in users |
inertia.encrypt | EncryptHistory | Encrypts the browser history state of the Inertia pages behind it |
precognitive | HandlePrecognitiveRequests | Answers a precognitive request once its data is validated (see precognitive requests) |
signed | ValidateSignature | Wants a valid, unexpired signed URL, else a 403; signed:relative checks a URL signed without its host |
throttle | ThrottleRequests | Limits a client's requests in a window of minutes, as throttle:60,1 for 60 a minute (see rate limiting) |
verified | EnsureEmailIsVerified | Wants a verified email address: JSON gets a 403, a page a redirect to the verification.notice route, or the route you name, as verified:profile.edit |
Redirecting guests and signed-in users#
redirectGuestsTo() sets where auth sends a guest, and redirectUsersTo() where guest sends a signed-in user.
Each takes a URL, or a function that receives the request:
Application.configure({ basePath: '.' }).withMiddleware((middleware) => { middleware.redirectGuestsTo('/login') middleware.redirectUsersTo((request) => (request.is('admin/*') ? '/admin' : '/dashboard'))})They set http.redirect_guests_to and http.redirect_users_to, which you can also set in config/; a new
app's sets redirect_guests_to to /login. Without a value, a guest goes to the login route, else /login, and a
signed-in user to the dashboard route, else the home route, else /.
Sorting middleware#
A request runs through the global middleware first, in their order, then the route's: its groups' middleware, outer group first, then its own. Among a route's middleware, the same middleware with the same parameters runs once, where it first appears.
Within a route's middleware, the web and api groups' middleware and auth and guest run first, keeping their
order. Then route model binding runs, then everything else. So can:update,post sees the bound Post, and a guest
is redirected before any database lookup. throttle is among everything else, so it runs after binding: a throttled
route with a model parameter looks the model up before it counts the request, and a missing model is a 404 that isn't
counted.
Middleware parameters#
A middleware can take parameters. They arrive as strings after next. This one wants the user to have one of the
roles it is given:
import { abort } from '@orieljs/framework'import type { HttpRequest, Middleware, Next } from '@orieljs/framework/contracts'import type { User } from '../../Models/User'export class EnsureUserHasRole implements Middleware { async handle(request: HttpRequest, next: Next, ...roles: string[]): Promise<Response> { const user = await request.user<User>() if (user === undefined || !roles.includes(user.role)) { abort(403) } return next(request) }}Give it an alias, then name the parameters after a :, separated by commas:
Application.configure({ basePath: '.' }).withMiddleware((middleware) => { middleware.alias({ role: EnsureUserHasRole })})Route.put('/posts/{id}', (id: string) => id).middleware('role:editor')Route.put('/posts/{id}', (id: string) => id).middleware('role:editor,publisher')Terminable middleware#
A middleware can do some work after the response has been sent to the browser. Give it a terminate() method, which
receives the request and the response:
import type { HttpRequest, Middleware, Next } from '@orieljs/framework/contracts'export class TerminatingMiddleware implements Middleware { async handle(request: HttpRequest, next: Next): Promise<Response> { return next(request) } async terminate(request: HttpRequest, response: Response): Promise<void> { // ... }}Register it as a global or a route middleware, as any other. On Node, terminate() runs once the response is
written; on Bun, right after it is handed to the server. An error in terminate() is reported, and doesn't reach the
user.
Precognitive requests#
The precognitive middleware lets a form check its data on the server before the user submits it, as Laravel's
Precognition does. A request with the Precognition: true header runs the route's action only up to its validation. The
React, Vue and Svelte Inertia forms send such requests when they validate; the Solid and
Angular forms don't offer it.
- When the data passes, the request ends there: a 204 with a
Precognition-Success: trueheader. - When it fails, the usual 422 with the errors.
A Precognition-Validate-Only: name,email header limits the check to those fields. Every response of the route varies
on Precognition, and a precognitive one carries a Precognition: true header.
Route.post('/users', async () => { const data = await request().validate({ name: 'required|string|max:255', email: 'required|email|unique:users,email', password: 'required|min:8', }) const user = await User.create(data) return redirect().route('users.show', user)}).middleware('precognitive')