Middleware and Errors
Spry keeps request behavior in two explicit places:
- middleware for cross-cutting flow control
- error handlers for translating failures into responses
Global middleware
Files in top-level middleware/ are loaded in filename order.
dart
// ignore_for_file: file_names
import 'package:spry/spry.dart';
Future<Response> middleware(Event event, Next next) async {
final startedAt = DateTime.now();
final response = await next();
final duration = DateTime.now().difference(startedAt).inMilliseconds;
print('${event.request.method} ${event.request.url.path} -> ${response.status} (${duration}ms)');
return response;
}This is the right place for:
- request logging
- tracing
- auth shells
- response timing
Scoped middleware
Use _middleware.dart inside routes/ when behavior should apply only to that branch of the route tree.
dart
import 'package:spry/spry.dart';
Future<Response> middleware(Event event, Next next) async {
event.locals.set(#requestId, DateTime.now().microsecondsSinceEpoch.toString());
return next();
}This is useful when a subset of routes needs shared locals, auth checks, or response wrapping.
Error handling
Use _error.dart to catch errors inside the current route scope and convert them into a stable response shape.
dart
import 'package:spry/spry.dart';
Response onError(Object error, StackTrace stackTrace, Event event) {
if (error case NotFoundError()) {
return Response.json({
'error': 'not_found',
'path': event.request.url.path,
}, status: 404);
}
if (error case HTTPError()) {
return error.toResponse();
}
return Response.json({
'error': 'internal_server_error',
'path': event.request.url.path,
}, status: 500);
}This is the clean path for:
- handling
NotFoundError - returning structured JSON errors
- avoiding repeated
try/catchblocks in handlers
Practical rule
- if it changes request flow across multiple routes, use middleware
- if it converts thrown errors into responses, use
_error.dart - if it belongs to one handler only, keep it inside that handler