Skip to content

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 → Cors

That 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:

Terminal window
maat make:middleware EnsureTokenIsValid

Registering 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 minute

An 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.