shelf

Shelf Framework Guide

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "shelf" with this command: npx skills add ar4mirez/samuel/ar4mirez-samuel-shelf

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 with addMiddleware and addHandler

  • Handler Simplicity: A Handler is just FutureOr<Response> Function(Request) -- keep it functional

  • Immutable Requests: Use request.change() to pass data downstream via context

  • Cascade Routing: Use Cascade to 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.dart is thin: load config, create handler, call shelf_io.serve , wire shutdown

  • lib/src/app.dart owns the Pipeline and Router composition

  • Handlers are organized by resource; each handler class exposes a Router get router

  • Middleware functions return Middleware (a typedef for Handler Function(Handler) )

  • Models use freezed for immutability and json_serializable for 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() before server.close() to release database connections

  • Keep main() under 25 lines

Configuration

  • Use a Config class with factory Config.fromEnvironment() reading from Platform.environment

  • Provide dev defaults for non-secret values (port, database URL)

  • Throw StateError in production if critical secrets are missing (JWT_SECRET, API keys)

  • Use const constructor 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 .call when mounting a Router or passing to addHandler

  • 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('/&#x3C;id>', _withAuth(_getById));
router.put('/&#x3C;id>', _withAuth(_update));
router.delete('/&#x3C;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() then jsonDecode()

  • Always set Content-Type: application/json on JSON responses

  • Return appropriate status codes: 201 for creation, 204 for deletion, 200 for reads/updates

  • Use shelf_router path 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), not Handler directly

  • 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.tryParse and 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-Type on 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)

  • Cascade treats 404 and 405 as "not handled" by default

  • Use statusCodes parameter 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 (not Error )

  • 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 shelf directly in tests (no HTTP server needed); mock services with mocktail

  • 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

  • Shelf Documentation

  • shelf_router

  • shelf_static

  • shelf_web_socket

  • Dart Server Tutorial

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

actix-web

No summary provided by upstream source.

Repository SourceNeeds Review
General

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

blazor

No summary provided by upstream source.

Repository SourceNeeds Review
General

fastapi

No summary provided by upstream source.

Repository SourceNeeds Review