Project Structure
Spry is easiest to understand when you start from the project tree, not from framework internals.
Default layout
text
.
├─ routes/
│ ├─ index.dart
│ ├─ users/[id].dart
│ ├─ _middleware.dart
│ └─ _error.dart
├─ middleware/
│ └─ 01_logger.dart
├─ public/
│ └─ hello.txt
├─ hooks.dart
└─ spry.config.dartEach part has a narrow job:
routes/defines route handlersmiddleware/defines global middlewarepublic/holds static assets served directlyhooks.dartdefines lifecycle hooksspry.config.dartconfigures target, output, and local runtime behavior
What belongs where
routes/
Put request handlers here. This is the center of the app.
dart
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,
});
}middleware/
Put global request behavior here when it should apply broadly across the app.
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;
}public/
Put static files here when they should be served without going through a route handler.
text
public/
robots.txt
logo.svg
hello.txthooks.dart
Put startup and shutdown behavior here:
dart
import 'package:spry/spry.dart';
Future<void> onStart(ServerLifecycleContext context) async {
print('Spry example started: ${context.runtime.name}');
}spry.config.dart
Put runtime choice and build behavior here:
dart
import 'package:spry/config.dart';
void main() {
defineSpryConfig(
host: '127.0.0.1',
port: 4000,
target: BuildTarget.dart,
);
}Why this structure works
Spry is opinionated about where files live so that the happy path stays obvious:
- route files are easy to scan
- cross-cutting behavior has a small number of entrypoints
- runtime choice is separated from route logic
That is the real authoring model. You should not need to think about generated internals during normal development.