Middleware
Middleware functions are the building blocks of request processing in Aim. They run before your route handlers and can modify requests, responses, or execute side effects like logging.
What is Middleware?
A middleware is a function that takes a Context and a Next function, and returns a Future<void>:
typedef Middleware<E extends Variables> = Future<void> Function(
Context<E> c,
Next next,
);- Context (
c): Provides access to the request and response - Next (
next): Calls the next middleware or route handler in the chain
A middleware cannot return a response
The return type is Future<void>, so return c.json(...) does not compile: c.json() evaluates to a Response. The response helpers (c.json(), c.text(), ...) finalize the response on the context, so calling one and returning without calling next() is enough to stop the chain and send it:
c.json({'error': 'Unauthorized'}, statusCode: 401);
return; // next() is not called - the finalized response is sentBasic Middleware
Here's a simple logging middleware:
import 'dart:io';
import 'package:aim_server/aim_server.dart';
Future<void> simpleLogger(Context c, Next next) async {
print('<-- ${c.req.method} ${c.req.path}');
await next();
print('--> ${c.req.method} ${c.req.path}');
}
void main() async {
final app = Aim();
// Use the middleware
app.use(simpleLogger);
app.get('/', (c) async => c.text('Hello!'));
await app.serve(host: InternetAddress.anyIPv4, port: 8080);
}Using Middleware
Add middleware with the use() method:
final app = Aim();
// Global middleware (applies to all routes)
app.use(logger());
app.use(cors());
// Routes
app.get('/', handler);Middleware Order
Middleware executes in the order it's defined:
final app = Aim();
app.use((c, next) async {
print('1. First middleware - before');
await next();
print('1. First middleware - after');
});
app.use((c, next) async {
print('2. Second middleware - before');
await next();
print('2. Second middleware - after');
});
app.get('/', (c) async {
print('3. Route handler');
return c.text('Hello!');
});
// Output:
// 1. First middleware - before
// 2. Second middleware - before
// 3. Route handler
// 2. Second middleware - after
// 1. First middleware - afterCalling Next
The next() function passes control to the next middleware or route handler:
// ✅ Always call next() if you want the chain to continue
app.use((c, next) async {
print('Before handler');
await next();
print('After handler');
});
// ✅ Return early without calling next() to stop the chain
app.use((c, next) async {
if (!isAuthorized(c)) {
c.json({'error': 'Unauthorized'}, statusCode: 401);
return; // next() is NOT called - chain stops here
}
return next();
});Modifying Context
Middleware can modify the context before passing it forward:
class MyVariables extends Variables {
String? requestId;
String? userId;
}
void main() async {
final app = Aim<MyVariables>(
variablesFactory: () => MyVariables(),
);
// Add request ID
app.use((c, next) async {
c.variables.requestId = DateTime.now().millisecondsSinceEpoch.toString();
return next();
});
// Add user ID from header
app.use((c, next) async {
final userId = c.req.headers['x-user-id'];
c.variables.userId = userId;
return next();
});
app.get('/', (c) async {
return c.json({
'requestId': c.variables.requestId,
'userId': c.variables.userId,
});
});
await app.serve(host: InternetAddress.anyIPv4, port: 8080);
}Setting Response Headers
Set headers that will be included in the final response:
app.use((c, next) async {
c.header('X-Request-ID', generateId());
c.header('X-Powered-By', 'Aim');
return next();
});Error Handling Middleware
Catch and handle errors in a centralized way:
Future<void> errorHandler(Context c, Next next) async {
try {
await next();
} catch (error, stackTrace) {
print('Error: $error');
print('Stack: $stackTrace');
c.json({
'error': 'Internal Server Error',
'message': error.toString(),
}, statusCode: 500);
}
}
void main() async {
final app = Aim();
// Add error handler first
app.use(errorHandler);
app.get('/error', (c) async {
throw Exception('Something went wrong!');
});
await app.serve(host: InternetAddress.anyIPv4, port: 8080);
}app.onError
A try/catch middleware only catches errors thrown while next() runs — it still has to be registered before every route that can throw. Aim also has a single, centralized hook:
typedef ErrorHandler<E extends Variables> = Future<Response> Function(
Object error,
Context<E> c,
);
Aim<E> onError(ErrorHandler<E> handler);final app = Aim();
app.onError((error, c) async {
return c.json({'error': error.toString()}, statusCode: 500);
});
app.get('/error', (c) async {
throw Exception('Something went wrong!');
});If no onError handler is registered, the behavior is adapter-specific: aim_server (the dart:io adapter) logs the error with its stack trace and responds with a plain 500. Whether the Workers, Deno, and Cloud Functions adapters do the same has not been separately verified here — check each runtime page if that matters for your deployment.
Authentication Middleware
app.use() appends to one middleware list that runs, in registration order, for every request that reaches routing — regardless of whether the middleware was registered before or after a given route. There is no way to make a middleware apply "only to routes registered after it". Registering requireAuth after /login does not exempt /login; the middleware itself must decide which requests to skip:
class AuthVariables extends Variables {
String? userId;
}
Future<void> requireAuth(Context<AuthVariables> c, Next next) async {
const publicPaths = {'/login', '/register'};
if (publicPaths.contains(c.req.path)) {
return next();
}
final token = c.req.headers['authorization'];
if (token == null) {
c.json({'error': 'Missing token'}, statusCode: 401);
return;
}
// Verify token and extract user ID
final userId = await verifyToken(token);
if (userId == null) {
c.json({'error': 'Invalid token'}, statusCode: 401);
return;
}
c.variables.userId = userId;
return next();
}
void main() async {
final app = Aim<AuthVariables>(
variablesFactory: () => AuthVariables(),
);
app.use(requireAuth);
app.post('/login', loginHandler); // skipped by requireAuth's own check
app.post('/register', registerHandler); // skipped by requireAuth's own check
app.get('/profile', profileHandler);
app.get('/dashboard', dashboardHandler);
await app.serve(host: InternetAddress.anyIPv4, port: 8080);
}aim_server_jwt's built-in middleware has the same constraint and solves it with JwtOptions.excludedPaths instead of an inline path check — see JWT Authentication.
Conditional Middleware
The same principle applies to any middleware that should only act on part of the route tree: check the path inside the middleware itself, since use() cannot scope by registration position.
void main() async {
final app = Aim();
// Apply to all routes
app.use(logger());
// Public routes
app.get('/', homeHandler);
app.get('/about', aboutHandler);
// Admin routes with auth middleware
app.use((c, next) async {
if (c.req.path.startsWith('/admin')) {
return requireAuth(c, next);
}
return next();
});
app.get('/admin/dashboard', adminHandler);
app.get('/admin/users', usersHandler);
await app.serve(host: InternetAddress.anyIPv4, port: 8080);
}Creating Reusable Middleware
Middleware factories allow configuration:
// Middleware factory
Middleware<E> timing<E extends Variables>() {
return (c, next) async {
final stopwatch = Stopwatch()..start();
await next();
stopwatch.stop();
c.header('X-Response-Time', '${stopwatch.elapsedMilliseconds}ms');
};
}
// Usage
final app = Aim();
app.use(timing());With parameters:
Middleware<E> ratelimit<E extends Variables>({
required int maxRequests,
required Duration window,
}) {
final requests = <String, List<DateTime>>{};
return (c, next) async {
final ip = c.req.headers['x-forwarded-for'] ?? 'unknown';
final now = DateTime.now();
requests[ip] ??= [];
requests[ip]!.removeWhere((t) => now.difference(t) > window);
if (requests[ip]!.length >= maxRequests) {
c.json(
{'error': 'Rate limit exceeded'},
statusCode: 429,
);
return;
}
requests[ip]!.add(now);
return next();
};
}
// Usage
final app = Aim();
app.use(ratelimit(maxRequests: 100, window: Duration(minutes: 1)));Built-in Middleware
Aim provides official middleware packages:
import 'package:aim_server_logger/aim_server_logger.dart';
import 'package:aim_server_cors/aim_server_cors.dart';
import 'package:aim_server_jwt/aim_server_jwt.dart';
final app = Aim<JwtVariables>(
variablesFactory: () => JwtVariables.create(
JwtOptions(
algorithm: HS256(
secretKey: SecretKey(secret: 'your-secret-key-at-least-32-characters'),
),
),
),
);
// Logger
app.use(logger());
// CORS
app.use(cors(CorsOptions(
origin: 'https://example.com',
allowMethods: ['GET', 'POST'],
)));
// JWT Authentication
app.use(jwt());See the Middleware Packages section for all available middleware.
Best Practices
- Order Matters: Security/auth middleware should run early
- Always Await: Use
await next()to ensure proper execution order - Early Return: Return without calling
next()to stop the chain - Type Safety: Use custom
Variablesclasses for typed context variables - Error Handling: Wrap
next()in try-catch for error handling - Single Responsibility: Each middleware should do one thing well
Common Patterns
Request Timing
app.use((c, next) async {
final stopwatch = Stopwatch()..start();
await next();
print('Request took ${stopwatch.elapsedMilliseconds}ms');
});Request/Response Logging
app.use((c, next) async {
print('${c.req.method} ${c.req.path}');
await next();
print('Response sent');
});Header Injection
app.use((c, next) async {
c.header('X-API-Version', '1.0');
c.header('X-Request-ID', generateId());
return next();
});Body Parsing
app.use((c, next) async {
if (c.req.headers['content-type']?.contains('application/json') ?? false) {
// Parse JSON body and store in context
final body = await c.req.json();
// Store for later use
}
return next();
});Next Steps
- Explore the Context API for request/response handling
- Browse Middleware Packages for ready-to-use middleware
- Learn about specific middleware like CORS and JWT
