Agent skill
shelf
Shelf framework guardrails, patterns, and best practices for AI-assisted development. Use when working with Shelf (Dart HTTP server) projects, or when the user mentions Shelf. Provides middleware patterns, request handling, pipeline composition, and server guidelines.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/shelf-ar4mirez-samuel
Metadata
Additional technical details for this skill
- author
- samuel
- version
- 1.0
- category
- framework
- language
- dart
- extensions
- .dart
SKILL.md
Shelf Framework Guide
Applies to: Shelf 1.x, Dart 3.x, REST APIs, Microservices, Backend Services Complements:
.claude/skills/dart-guide/SKILL.md
Core Principles
- Middleware Composition: Everything flows through
Pipeline; compose handlers withaddMiddlewareandaddHandler - Handler Simplicity: A
Handleris justFutureOr<Response> Function(Request)-- keep it functional - Immutable Requests: Use
request.change()to pass data downstream viacontext - Cascade Routing: Use
Cascadeto try multiple handlers in sequence until one succeeds - Separation of Concerns: Handlers call services, services call repositories -- no business logic in middleware
Project Structure
myapp/
├── bin/
│ └── server.dart # Entry point (thin: config, serve, shutdown)
├── lib/
│ ├── src/
│ │ ├── app.dart # Pipeline + Router assembly
│ │ ├── config/
│ │ │ └── config.dart # Environment-based configuration
│ │ ├── handlers/
│ │ │ ├── health_handler.dart
│ │ │ └── users_handler.dart
│ │ ├── middleware/
│ │ │ ├── auth_middleware.dart
│ │ │ ├── cors_middleware.dart
│ │ │ └── logging_middleware.dart
│ │ ├── models/
│ │ │ └── user.dart
│ │ ├── repositories/
│ │ │ └── user_repository.dart
│ │ └── services/
│ │ └── user_service.dart
│ └── myapp.dart # Library barrel export
├── test/
│ ├── handlers/
│ │ └── users_handler_test.dart
│ └── middleware/
│ └── auth_middleware_test.dart
├── pubspec.yaml
├── analysis_options.yaml
└── Dockerfile
Architectural rules:
bin/server.dartis thin: load config, create handler, callshelf_io.serve, wire shutdownlib/src/app.dartowns the Pipeline and Router composition- Handlers are organized by resource; each handler class exposes a
Router get router - Middleware functions return
Middleware(a typedef forHandler Function(Handler)) - Models use
freezedfor immutability andjson_serializablefor serialization - Services contain business logic; repositories handle data access
Dependencies (pubspec.yaml)
dependencies:
shelf: ^1.4.0
shelf_router: ^1.1.0
shelf_static: ^1.1.0 # Static file serving
shelf_web_socket: ^1.0.0 # WebSocket support
# Data
freezed_annotation: ^2.4.0
json_annotation: ^4.8.0
# Auth
dart_jsonwebtoken: ^2.12.0
bcrypt: ^1.1.0
dev_dependencies:
test: ^1.24.0
mocktail: ^1.0.0
build_runner: ^2.4.0
freezed: ^2.4.0
json_serializable: ^6.7.0
Application Entry Point
// bin/server.dart
import 'dart:io';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:myapp/myapp.dart';
Future<void> main() async {
final config = Config.fromEnvironment();
final app = Application(config);
final handler = await app.createHandler();
final server = await shelf_io.serve(
handler,
InternetAddress.anyIPv4,
config.port,
);
print('Server running on http://${server.address.host}:${server.port}');
// Graceful shutdown
ProcessSignal.sigint.watch().listen((_) async {
print('Shutting down...');
await app.close();
await server.close();
exit(0);
});
}
Entry Point Rules
- Load configuration from environment variables (never hardcode secrets)
- Wire graceful shutdown via
ProcessSignal.sigint - Call
app.close()beforeserver.close()to release database connections - Keep
main()under 25 lines
Configuration
- Use a
Configclass withfactory Config.fromEnvironment()reading fromPlatform.environment - Provide dev defaults for non-secret values (port, database URL)
- Throw
StateErrorin production if critical secrets are missing (JWT_SECRET, API keys) - Use
constconstructor for Config to enable compile-time checks - Never log secret values
Application Setup (Pipeline + Router)
// lib/src/app.dart
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
class Application {
final Config config;
late final UserRepository _userRepository;
late final UserService _userService;
Application(this.config);
Future<Handler> createHandler() async {
_userRepository = UserRepository();
_userService = UserService(_userRepository, config);
final healthHandler = HealthHandler();
final usersHandler = UsersHandler(_userService);
final router = Router()
..mount('/health', healthHandler.router.call)
..mount('/api/v1/users', usersHandler.router.call);
final pipeline = const Pipeline()
.addMiddleware(loggingMiddleware())
.addMiddleware(corsMiddleware())
.addMiddleware(handleErrors())
.addHandler(router.call);
return pipeline;
}
Future<void> close() async {
await _userRepository.close();
}
}
Pipeline Rules
- Middleware order: logging -> CORS -> error handling -> router
- Logging outermost so it captures all requests including CORS preflight
- Error handling wraps the router so thrown exceptions are caught
- Always call
.callwhen mounting aRouteror passing toaddHandler - Use
const Pipeline()for the initial empty pipeline
Handlers
Handlers are classes that expose a Router get router property. Each route method receives a Request and returns a Response.
// lib/src/handlers/users_handler.dart
class UsersHandler {
final UserService _userService;
UsersHandler(this._userService);
Router get router {
final router = Router();
// Public routes
router.post('/register', _register);
router.post('/login', _login);
// Protected routes
router.get('/', _withAuth(_getAll));
router.get('/<id>', _withAuth(_getById));
router.put('/<id>', _withAuth(_update));
router.delete('/<id>', _withAuth(_delete));
return router;
}
Handler _withAuth(Handler handler) {
return const Pipeline()
.addMiddleware(authMiddleware())
.addHandler(handler);
}
Future<Response> _register(Request request) async {
final body = await request.readAsString();
final json = jsonDecode(body) as Map<String, dynamic>;
final user = await _userService.register(
email: json['email'] as String,
password: json['password'] as String,
name: json['name'] as String,
);
return Response(
201,
body: jsonEncode(user.toJson()),
headers: {'Content-Type': 'application/json'},
);
}
}
Handler Rules
- One handler class per resource (UsersHandler, ProductsHandler)
- Keep handler methods under 20 lines; delegate business logic to services
- Use
_withAuth(handler)helper to wrap individual routes with auth middleware - Parse request body with
request.readAsString()thenjsonDecode() - Always set
Content-Type: application/jsonon JSON responses - Return appropriate status codes:
201for creation,204for deletion,200for reads/updates - Use
shelf_routerpath parameters with angle brackets:/<id>
Middleware
A Middleware is a function that takes a Handler and returns a new Handler.
// Middleware type signature
typedef Middleware = Handler Function(Handler innerHandler);
Writing Middleware
Middleware loggingMiddleware() {
return (Handler innerHandler) {
return (Request request) async {
final stopwatch = Stopwatch()..start();
print('[${DateTime.now()}] ${request.method} ${request.requestedUri}');
final response = await innerHandler(request);
stopwatch.stop();
print(
'[${DateTime.now()}] ${request.method} ${request.requestedUri} '
'${response.statusCode} ${stopwatch.elapsedMilliseconds}ms',
);
return response;
};
};
}
Error Handling Middleware
Middleware handleErrors() {
return (Handler innerHandler) {
return (Request request) async {
try {
return await innerHandler(request);
} on NotFoundException catch (e) {
return _jsonError(404, e.message);
} on ValidationException catch (e) {
return _jsonError(422, e.message, errors: e.errors);
} on UnauthorizedException {
return _jsonError(403, 'Unauthorized');
} catch (e, stack) {
print('Error: $e\n$stack');
return _jsonError(500, 'Internal server error');
}
};
};
}
Response _jsonError(int status, String message, {Map<String, dynamic>? errors}) {
return Response(
status,
body: jsonEncode({
'error': message,
if (errors != null) 'errors': errors,
}),
headers: {'Content-Type': 'application/json'},
);
}
Middleware Rules
- Return
Middleware(a function), notHandlerdirectly - Always
await innerHandler(request)-- never skip calling the inner handler unless short-circuiting (auth failure, rate limit) - Use
request.change(context: {...request.context, 'key': value})to pass data downstream - Error-handling middleware must catch all exceptions and return proper HTTP responses
- Never let unhandled exceptions propagate to
shelf_io.serve
Request and Response
Reading Request Data
| Source | How | Example |
|---|---|---|
| Body | await request.readAsString() then jsonDecode() |
final json = jsonDecode(await request.readAsString()) |
| Query params | request.url.queryParameters['key'] |
int.tryParse(request.url.queryParameters['page'] ?? '1') ?? 1 |
| Path params | Extra function arguments (shelf_router) | Future<Response> _getById(Request request, String id) |
| Headers | request.headers['Header-Name'] |
request.headers['Authorization'] |
| Context | request.context['key'] |
request.context['user'] as User |
Building Responses
| Status | Method |
|---|---|
| 200 OK | Response.ok(jsonEncode(data), headers: {'Content-Type': 'application/json'}) |
| 201 Created | Response(201, body: jsonEncode(data), headers: {'Content-Type': 'application/json'}) |
| 204 No Content | Response(204) |
| 404 Not Found | Response.notFound(jsonEncode({'error': 'Not found'}), headers: {'Content-Type': 'application/json'}) |
| Modify response | response.change(headers: {...response.headers, 'X-Custom': 'value'}) |
Request/Response Rules
- Parse query parameters defensively with
int.tryParseand defaults - Read the body only once (it is a stream); do not call
readAsString()twice - Use
request.change(context:)for downstream data, never mutable globals - Set
Content-Typeon every response that has a body
Routing with shelf_router
final router = Router()
..get('/health', _health)
..get('/users', _listUsers)
..get('/users/<id>', _getUser)
..post('/users', _createUser)
..put('/users/<id>', _updateUser)
..delete('/users/<id>', _deleteUser);
// Mount sub-routers with path prefix
final root = Router()
..mount('/api/v1', apiRouter.call)
..mount('/ws', webSocketHandler);
Routing Rules
- Use
..method('/path', handler)cascade syntax for readability - Mount sub-routers with
..mount('/prefix', router.call) - Path parameters use angle brackets:
/<id>,/<slug> - Always version API routes:
/api/v1/... - Group related routes in a single handler class
Cascade (Fallback Routing)
Cascade tries handlers in order until one returns a non-404 response.
import 'package:shelf/shelf.dart';
import 'package:shelf_static/shelf_static.dart';
final cascade = Cascade()
.add(apiRouter)
.add(createStaticHandler('public', defaultDocument: 'index.html'));
final handler = const Pipeline()
.addMiddleware(loggingMiddleware())
.addHandler(cascade.handler);
Cascade Rules
- Place specific handlers (API) before generic handlers (static files)
Cascadetreats 404 and 405 as "not handled" by default- Use
statusCodesparameter to customize which codes trigger fallthrough - Useful for SPAs: API routes first, then static file handler as fallback
Custom Exception Types
class UnauthorizedException implements Exception {
final String message;
UnauthorizedException([this.message = 'Unauthorized']);
}
class NotFoundException implements Exception {
final String message;
NotFoundException(this.message);
}
class ValidationException implements Exception {
final String message;
final Map<String, List<String>> errors;
ValidationException(this.message, [this.errors = const {}]);
}
Exception Rules
- Define domain exceptions that implement
Exception(notError) - Map exceptions to HTTP status codes in error-handling middleware only
- Never throw generic
Exception('message')-- use typed exceptions - Include structured error details (field-level validation errors)
Commands
# Development
dart run bin/server.dart # Start server
dart run --enable-vm-service bin/server.dart # With debugger
# Code generation (freezed, json_serializable)
dart run build_runner build --delete-conflicting-outputs
dart run build_runner watch # Watch mode for codegen
# Testing
dart test # Run all tests
dart test test/handlers/ # Run specific directory
dart test --coverage # With coverage
# Quality
dart format . # Format all files
dart analyze # Static analysis
dart fix --apply # Auto-fix lint issues
# Build
dart compile exe bin/server.dart -o server # AOT compile to native binary
Best Practices Summary
- Pipeline: Compose middleware with
Pipeline; order matters (outermost runs first) - Handlers: Keep thin; delegate to services; one class per resource
- Error Handling: Centralize in middleware; catch all exceptions; return structured JSON errors
- Context: Pass data between middleware and handlers via
request.change(context:) - Testing: Use
shelfdirectly in tests (no HTTP server needed); mock services withmocktail - Security: Validate all inputs; use parameterized database queries; never log secrets
- Performance: Compile to native executable for production; use streaming for large responses
Advanced Topics
For detailed middleware examples, WebSocket support, static files, authentication flows, rate limiting, and testing patterns, see:
- references/patterns.md -- Full middleware patterns, WebSocket handler, static file serving, JWT auth, rate limiting, repository pattern, integration testing, Dockerfile
External References
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?