Getting Started
Spry starts with a Dart project, a routes/ directory, and a config file that tells the generated app where it should run.
Install the package
dart pub add spryCreate the runtime config
This is the control plane for local serve and production builds:
import 'package:spry/config.dart';
void main() {
defineSpryConfig(host: '127.0.0.1', port: 4000, target: BuildTarget.vm);
}Add your first route
Create routes/index.dart:
import 'package:spry/spry.dart';
Response handler(Event event) {
return Response.json({
'message': 'hello from spry',
'runtime': event.context.runtime.name,
'path': event.url.path,
});
}Spry treats this file as the handler for /.
Start the dev server
dart run spry serveBy default, the generated app runs from your current project root and follows the values defined in spry.config.dart.
Grow the route tree
You do not register routes imperatively. You add files:
routes/
index.dart
about.get.dart
users/[id].dart
[...slug].dart
_middleware.dart
_error.dartabout.get.dart
import 'package:spry/spry.dart';
Response handler(Event event) {
return Response.json({'page': 'about', 'method': event.method});
}users/[id].dart
// 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()});
}Add middleware and scoped error handling
Middleware and error files are part of the same tree:
_middleware.dart
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
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.url.path,
}, ResponseInit(status: 404));
}
if (error case HTTPError()) {
return error.toResponse();
}
return Response.json({
'error': 'internal_server_error',
'path': event.url.path,
}, ResponseInit(status: 500));
}_middleware.dart applies to the current directory scope. You can also use _middleware.get.dart, _middleware.post.dart, and the other supported method suffixes to scope middleware to a single request method. _error.dart catches errors raised by routes within the same scope.
Keep one-off behavior inside one route
If a small middleware chain or error mapping belongs to one handler only, use defineHandler(...) in that route file instead of adding new scoped files:
import 'package:spry/spry.dart';
final handler = defineHandler(
(event) => Response.json({'ok': true}),
middleware: [
(event, next) async {
if (event.headers.get('x-demo') == null) {
throw const HTTPError(400, body: 'missing x-demo');
}
return next();
},
],
onError: (error, stackTrace, event) {
if (error case HTTPError()) {
return error.toResponse();
}
rethrow;
},
);Use defineHandler(...) for one route. Use _middleware.dart and _error.dart when the behavior belongs to a whole branch.
Build for production
When you are ready to generate the app entrypoint:
dart run spry buildSpry scans your project and writes the generated runtime output into .spry/ by default.
What to read next
- Project Structure shows how a real Spry project is organized.
- File Routing explains naming, params, wildcard files, and scope rules.
- OpenAPI shows how to generate
openapi.jsonfrom config and route metadata. - Middleware and Errors covers cross-cutting request behavior.
- WebSockets shows how to upgrade from a normal route handler.
- Assets explains static files.
- Lifecycle covers hooks and request order.
- Deploy Overview covers Dart, Node, Bun, Cloudflare Workers, and Vercel.