File Routing
Spry v7 does not want you registering routes by hand. The filesystem is the source of truth.
Start from routes/
The scanner walks every Dart file under routes/ and turns it into a route, scoped middleware binding, or scoped error boundary.
routes/
index.dart
about.get.dart
users/[id].dart
[...slug].dart
_middleware.dart
_error.dartRoute files
index.dart
routes/index.dart maps to /.
import 'package:spry/spry.dart';
Response handler(Event event) {
return Response.json({
'message': 'hello from spry',
'runtime': event.context.runtime.name,
'path': event.request.url.path,
});
}Method-specific files
Append an HTTP method to restrict a file to that method:
import 'package:spry/spry.dart';
Response handler(Event event) {
return Response.json({
'page': 'about',
'method': event.method,
});
}That file maps to GET /about.
Supported suffixes are:
.get.post.put.patch.delete.head.options
Dynamic params
Square brackets create named params:
// ignore_for_file: file_names
import 'package:spry/spry.dart';
Response handler(Event event) {
final id = event.params.required('id');
return Response.json({
'id': id,
'upper': id.toUpperCase(),
});
}routes/users/[id].dart maps to /users/:id.
Catch-all files
Use [...name].dart for remainder matches:
import 'package:spry/spry.dart';
Response handler(Event event) {
return Response.json({
'fallback': true,
'slug': event.params.wildcard,
}, status: 404);
}The wildcard value is available through event.params.wildcard.
routes/[...slug].dart maps to /**:slug.
Expressive segment syntax
Spry now forwards richer pathname syntax into roux through filesystem-safe file names:
| File or folder name | Route segment |
|---|---|
[id] | :id |
[id([0-9]+)] | :id([0-9]+) |
[[id]] | :id? |
[...slug] | **:slug |
[...] | ** |
[...path+] | :path+ |
[[...path]] | :path* |
[_] | * |
[name].[ext] | :name.:ext |
post-[id].json | post-:id.json |
Notes:
[...name]is a terminal remainder matcher. It can only appear at the end of a route.[_]is a single-segment wildcard. It matches exactly one path segment.[[name]],[...path+], and[[...path]]let one file cover optional or repeated suffix segments.- Embedded params work anywhere inside a segment, so dots and literal prefixes stay intact.
Scoped files
_middleware.dart
This file wraps all matching routes in the current directory scope:
import 'package:spry/spry.dart';
Future<Response> middleware(Event event, Next next) async {
event.locals.set(#requestId, DateTime.now().microsecondsSinceEpoch.toString());
return next();
}_error.dart
This file catches errors thrown by matching routes in the current scope:
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);
}Global middleware
Files in top-level middleware/ are collected separately and executed in filename order:
// 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;
}Scope rules that matter
- Files or folders that start with
_are reserved for framework behavior. - Catch-all directories cannot also define scoped middleware or scoped error files under the same wildcard shape.
- Spry rejects duplicate routes and param-name drift for the same route shape during scanning.
- The root fallback is the root-level catch-all route when present.