Middleware Guide
This guide will walk you through the steps to create and use middleware in your Finch application. Middleware provides a convenient mechanism for inspecting and filtering HTTP requests entering your application.
What is Middleware?
Middleware in Finch is a class that sits between the incoming HTTP request and your route handler (controller or index function). It allows you to run logic before a request reaches your controller — for example, checking headers, logging requests, validating tokens, modifying request parameters, or blocking unauthorized access.
Each middleware class must extend the abstract Middleware class and implement the handle() method.
How Middleware Works
When a request matches a route that has middleware attached, Finch executes the middleware pipeline in order before passing the request to the controller or index function. The flow is:
- Route pattern matching
- HTTP method, host, and port validation
- Authentication check (
auth) - Middleware execution (runs each middleware's
handle()method in sequence) - Permission check
- Controller/index execution
Return Values
The handle() method returns a Future<String?>:
- Return
null— the request passes through and continues to the next middleware or to the route handler. - Return a non-empty
String— the request is blocked. The middleware pipeline stops, and the route is not matched.
If multiple middlewares are attached to a route, they are executed sequentially. If any middleware returns a non-empty string, the remaining middlewares are skipped and the request is rejected.
Creating a Middleware
To create a middleware, extend the Middleware class and override the handle() method:
import 'package:finch/finch_route.dart';
class MyMiddleware extends Middleware {
@override
Future<String?> handle() async {
// Your logic here
return null; // Allow the request to proceed
}
}
Accessing the Request
Inside a middleware, you can access the current request using rq (provided by the Middleware base class):
class LogMiddleware extends Middleware {
@override
Future<String?> handle() async {
print('Request: ${rq.method} ${rq.uri.path}');
return null;
}
}
Adding Parameters
You can inject data into the request from a middleware using rq.addParam(). This data will be available to controllers and templates downstream:
class TestMiddleware extends Middleware {
@override
Future<String?> handle() async {
rq.addParam('middleware', 'Test Middleware Active');
return null;
}
}
Blocking a Request
Return a non-empty string to block the request from reaching the route handler:
class ApiKeyMiddleware extends Middleware {
@override
Future<String?> handle() async {
final apiKey = rq.header('X-API-Key');
if (apiKey != 'my-secret-key') {
rq.renderError(401, message: 'Invalid API key');
return 'Unauthorized'; // Blocks the request
}
return null; // Allow the request to proceed
}
}
Attaching Middleware to Routes
There are two ways to attach middleware to a route.
1. Using the Constructor
Pass a list of middleware instances to the middlewares parameter of FinchRoute:
final testMiddleware = TestMiddleware();
final logMiddleware = LogMiddleware();
FinchRoute(
path: '/info',
index: homeController.info,
middlewares: [testMiddleware, logMiddleware],
);
2. Using the Fluent API
Use the .middleware() method to chain middleware onto a route:
FinchRoute(
path: '/info',
index: homeController.info,
).middleware(TestMiddleware()).middleware(LogMiddleware());
Middleware on Parent Routes
When middleware is attached to a parent route that has children, the middleware runs before any child route is processed. This allows you to apply shared logic (such as authentication or logging) to an entire group of routes:
FinchRoute(
path: '/admin',
middlewares: [AdminMiddleware()],
children: [
FinchRoute(
path: '/dashboard',
index: adminController.dashboard,
),
FinchRoute(
path: '/users',
index: adminController.users,
),
],
);
In this example, AdminMiddleware will execute before any request to /admin/dashboard or /admin/users.
Practical Examples
Rate Limiting Middleware
class RateLimitMiddleware extends Middleware {
static final Map<String, List<DateTime>> _requests = {};
final int maxRequests;
final Duration window;
RateLimitMiddleware({
this.maxRequests = 100,
this.window = const Duration(minutes: 1),
});
@override
Future<String?> handle() async {
final ip = rq.clientIP;
final now = DateTime.now();
_requests[ip] = (_requests[ip] ?? [])
..removeWhere((t) => now.difference(t) > window)
..add(now);
if (_requests[ip]!.length > maxRequests) {
rq.renderError(429, message: 'Too many requests');
return 'Rate limit exceeded';
}
return null;
}
}
CORS Middleware
class CorsMiddleware extends Middleware {
final String allowedOrigin;
CorsMiddleware({this.allowedOrigin = '*'});
@override
Future<String?> handle() async {
rq.response.headers.add('Access-Control-Allow-Origin', allowedOrigin);
rq.response.headers.add('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
rq.response.headers.add('Access-Control-Allow-Headers', 'Content-Type, Authorization');
return null;
}
}
Maintenance Mode Middleware
class MaintenanceMiddleware extends Middleware {
final bool isEnabled;
MaintenanceMiddleware({this.isEnabled = false});
@override
Future<String?> handle() async {
if (isEnabled) {
rq.renderError(503, message: 'Service is under maintenance');
return 'Maintenance mode';
}
return null;
}
}
Middleware vs Auth Controller
| Feature | Middleware | Auth Controller |
|---|---|---|
| Purpose | General-purpose request filtering | Authentication & authorization |
| Execution Order | After auth, before permissions | Before middleware |
| Return Type | Future<String?> |
Future<bool> |
| Can modify request | Yes (rq.addParam) |
Yes |
| Can render errors | Yes | Yes |
| Multiple per route | Yes (list) | One per route |
| Inherited by children | Yes | Yes |
Use Auth Controller when you need full authentication and session management. Use Middleware for everything else — logging, header validation, rate limiting, CORS, feature flags, etc.
Full Example
Here is a complete example of using middleware in a Finch application:
1. Create middleware classes:
// lib/middleware/test_middleware.dart
import 'package:finch/finch_route.dart';
class TestMiddleware extends Middleware {
@override
Future<String?> handle() async {
rq.addParam('middleware', 'Test Middleware Active');
return null;
}
}
2. Register middleware in your routes:
// lib/route/web_route.dart
import '../middleware/test_middleware.dart';
final testMiddleware = TestMiddleware();
List<FinchRoute> routes = [
FinchRoute(
key: 'root.info',
path: 'info',
extraPath: ['api/info'],
index: homeController.info,
middlewares: [testMiddleware],
),
];
3. Access middleware data in your controller:
class HomeController extends Controller {
Future<String> info() async {
final middlewareParam = rq.get('middleware');
return rq.renderString(text: 'Middleware says: $middlewareParam');
}
}
API Reference
Middleware (abstract class)
| Member | Type | Description |
|---|---|---|
rq |
Request (getter) |
Access the current HTTP request object |
handle() |
Future<String?> |
Override to implement middleware logic. Return null to pass, non-empty string to block. |
FinchRoute Middleware Members
| Member | Type | Description |
|---|---|---|
middlewares |
List<Middleware> |
List of middleware instances attached to the route |
middleware(m) |
FinchRoute |
Fluent method to add a middleware; returns the route for chaining |
handleMiddlewares() |
Future<bool> |
Executes all middleware in order; returns true if all passed |