Middleware
Middleware sit between the server and your route. They can allow a request to continue, stop it early, or adjust the response on its way back out.
class EnsureAdmin extends Middleware { @override Future<Response> handle(Request request, Next next) async { if (request.attributes['role'] != 'admin') { throw ForbiddenHttpException('Administrator access is required.'); }
return next(request); }}Authentication, CORS headers, rate limiting and input normalisation all belong here because each concern can wrap many routes without being copied into every handler.
The Middleware Pipeline
Think of the pipeline as nested layers. Middleware run from the outside in before the handler, then unwind in reverse order after it returns:
Cors → Authenticate → Handler → Authenticate → CorsThat order lets an outer middleware attach a header even when an inner layer returns early or throws an exception.
Writing Middleware
class EnsureTokenIsValid extends Middleware { @override Future<Response> handle(Request request, Next next) async { if (request.bearerToken() != 'secret') { throw UnauthorizedHttpException(); }
return next(request); }}Call next to continue down the pipeline. Anything you do before that call happens on the way in; anything after it happens on the way out:
class Timing extends Middleware { @override Future<Response> handle(Request request, Next next) async { final started = DateTime.now(); final response = await next(request); final ms = DateTime.now().difference(started).inMilliseconds;
return response.header('x-response-time', '${ms}ms'); }}To stop the request, simply do not call next — return a response instead, or throw an HTTP exception.
Generate one with:
maat make:middleware EnsureTokenIsValidRegistering Middleware
Middleware are configured in bootstrap/app.dart:
.withMiddleware((middleware) => middleware .use([TrimStrings(), Cors()]) .alias({ 'auth': EnsureTokenIsValid(), 'throttle': ThrottleRequests.factory, }))| Method | Description |
|---|---|
use([...]) |
Replace the global stack. Runs on every request. |
append(m) |
Add to the end of the global stack. |
prepend(m) |
Add to the front of the global stack. |
alias({...}) |
Give middleware short names for use on routes. |
Assigning Middleware to Routes
Route.get('/profile', show).middleware(['auth']);
Route.group(() { Route.get('/posts', index); Route.post('/posts', store);}, middleware: ['auth', 'throttle:60,1']);Global middleware run first, followed by group, controller, and route middleware, then the handler. The response travels back through those layers in reverse.
Middleware Parameters
An alias may point at a factory that receives the parameters written after the colon:
.alias({'throttle': ThrottleRequests.factory})Route.get('/search', search).middleware(['throttle:30,1']);The parameters arrive as a List<String> — here ['30', '1'], meaning thirty requests per one minute.
An alias may be any of three things: a Middleware instance, a plain function (request, next) => ..., or a factory Middleware Function(List<String>). Only a factory may take parameters; passing parameters to the other two is an error you will see at boot.
Included Middleware
TrimStrings
Trims whitespace from every incoming string value, including nested ones. Empty strings are left as empty strings, not converted to null.
Cors
Adds CORS headers and answers preflight OPTIONS requests. Configure it in config/cors.dart:
final cors = { 'paths': ['*'], 'allowed_origins': ['*'], 'allowed_methods': ['*'], 'allowed_headers': ['*'], 'exposed_headers': <String>[], 'max_age': 0, 'supports_credentials': false,};| Key | Default | Description |
|---|---|---|
paths |
['*'] |
Which paths CORS applies to. |
allowed_origins |
['*'] |
Permitted origins. |
allowed_methods |
['*'] |
Permitted verbs. |
allowed_headers |
['*'] |
Permitted request headers. |
exposed_headers |
[] |
Headers the browser may read. |
max_age |
0 |
How long a preflight may be cached, in seconds. |
supports_credentials |
false |
Allow cookies and credentials. |
With supports_credentials enabled, the specific request origin is reflected rather than *, because browsers reject the wildcard on credentialed requests.
ThrottleRequests
Limits requests per client, per route:
Route.get('/search', search).middleware(['throttle:60,1']); // 60 per minuteAn exceeded limit produces a 429 carrying retry-after and x-ratelimit-* headers.
The rate-limit counters live in memory, so they are per-process — two instances behind a load balancer each keep their own. Expired windows are evicted, so the state does not grow without bound. A shared store is a later addition.
Error Handling Inside the Pipeline
An exception thrown anywhere — a middleware, the handler, a service beneath it — is converted to a response inside the pipeline, not outside it. That means outer middleware still run on the way out, so a 500 still receives its CORS headers. Without this, a browser would report a CORS failure instead of showing you the real error.
The Pipeline Directly
Pipeline is available if you want to run a stack of middleware around something of your own:
final response = await Pipeline([TrimStrings(), Cors()]) .run(request, (request) => Response.json({'ok': true}));Each run builds its own chain, so one Pipeline may be reused and run concurrently without interference.