shelf
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseShelf Framework Guide
Shelf框架指南
Applies to: Shelf 1.x, Dart 3.x, REST APIs, Microservices, Backend Services Complements:.claude/skills/dart-guide/SKILL.md
适用范围:Shelf 1.x、Dart 3.x、REST API、微服务、后端服务 补充文档:.claude/skills/dart-guide/SKILL.md
Core Principles
核心原则
- Middleware Composition: Everything flows through ; compose handlers with
PipelineandaddMiddlewareaddHandler - Handler Simplicity: A is just
Handler-- keep it functionalFutureOr<Response> Function(Request) - Immutable Requests: Use to pass data downstream via
request.change()context - Cascade Routing: Use to try multiple handlers in sequence until one succeeds
Cascade - Separation of Concerns: Handlers call services, services call repositories -- no business logic in middleware
- 中间件编排:所有流程都通过流转;使用
Pipeline和addMiddleware组合处理函数addHandler - 处理函数简洁性:本质就是
Handler,保持其函数式特性FutureOr<Response> Function(Request) - 不可变请求:使用通过
request.change()向下游传递数据context - 级联路由:使用按顺序尝试多个处理函数,直到有一个成功响应
Cascade - 关注点分离:处理函数调用服务层,服务层调用仓库层,中间件中不要包含业务逻辑
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
└── DockerfileArchitectural rules:
- is thin: load config, create handler, call
bin/server.dart, wire shutdownshelf_io.serve - owns the Pipeline and Router composition
lib/src/app.dart - Handlers are organized by resource; each handler class exposes a
Router get router - Middleware functions return (a typedef for
Middleware)Handler Function(Handler) - Models use for immutability and
freezedfor serializationjson_serializable - Services contain business logic; repositories handle data access
myapp/
├── bin/
│ └── server.dart # 入口文件(代码精简:仅做配置、启动、关停逻辑)
├── lib/
│ ├── src/
│ │ ├── app.dart # Pipeline + Router 组装逻辑
│ │ ├── config/
│ │ │ └── config.dart # 基于环境变量的配置
│ │ ├── 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 # 库的桶导出文件
├── test/
│ ├── handlers/
│ │ └── users_handler_test.dart
│ └── middleware/
│ └── auth_middleware_test.dart
├── pubspec.yaml
├── analysis_options.yaml
└── Dockerfile架构规则:
- 代码保持精简:仅加载配置、创建处理函数、调用
bin/server.dart、绑定关停逻辑shelf_io.serve - 负责Pipeline和Router的组装
lib/src/app.dart - 处理函数按资源分类组织,每个处理函数类都暴露一个属性
Router get router - 中间件函数返回类型(是
Middleware的类型定义)Handler Function(Handler) - 模型使用实现不可变性,使用
freezed实现序列化json_serializable - 服务层包含业务逻辑,仓库层负责数据访问
Dependencies (pubspec.yaml)
依赖(pubspec.yaml)
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.0yaml
dependencies:
shelf: ^1.4.0
shelf_router: ^1.1.0
shelf_static: ^1.1.0 # 静态文件服务
shelf_web_socket: ^1.0.0 # WebSocket支持
# 数据处理
freezed_annotation: ^2.4.0
json_annotation: ^4.8.0
# 认证
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.0Application Entry Point
应用入口点
dart
// 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);
});
}dart
// 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}');
// 优雅关停
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 before
app.close()to release database connectionsserver.close() - Keep under 25 lines
main()
- 从环境变量加载配置(绝对不要硬编码密钥)
- 通过绑定优雅关停逻辑
ProcessSignal.sigint - 在调用前先调用
server.close()释放数据库连接app.close() - 保持函数代码行数不超过25行
main()
Configuration
配置
- Use a class with
Configreading fromfactory Config.fromEnvironment()Platform.environment - Provide dev defaults for non-secret values (port, database URL)
- Throw in production if critical secrets are missing (JWT_SECRET, API keys)
StateError - Use constructor for Config to enable compile-time checks
const - Never log secret values
- 使用类,通过
Config从factory Config.fromEnvironment()读取配置Platform.environment - 为非敏感配置提供开发环境默认值(端口、数据库URL)
- 生产环境下如果关键密钥缺失(JWT_SECRET、API密钥)要抛出
StateError - 为Config类使用构造函数以支持编译时检查
const - 绝对不要打印日志输出敏感配置值
Application Setup (Pipeline + Router)
应用初始化(Pipeline + Router)
dart
// 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();
}
}dart
// 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
Pipeline规则
- 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 when mounting a
.callor passing toRouteraddHandler - Use for the initial empty pipeline
const Pipeline()
- 中间件顺序:日志 -> CORS -> 错误处理 -> 路由
- 日志放在最外层,确保可以捕获所有请求包括CORS预检请求
- 错误处理包裹路由,确保抛出的异常都能被捕获
- 挂载或者传递给
Router时必须调用addHandler方法.call - 初始化空管道时使用
const Pipeline()
Handlers
处理函数(Handlers)
Handlers are classes that expose a property. Each route method receives a and returns a .
Router get routerRequestResponsedart
// 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'},
);
}
}处理函数是暴露属性的类,每个路由方法接收并返回。
Router get routerRequestResponsedart
// lib/src/handlers/users_handler.dart
class UsersHandler {
final UserService _userService;
UsersHandler(this._userService);
Router get router {
final router = Router();
// 公开路由
router.post('/register', _register);
router.post('/login', _login);
// 受保护路由
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 helper to wrap individual routes with auth middleware
_withAuth(handler) - Parse request body with then
request.readAsString()jsonDecode() - Always set on JSON responses
Content-Type: application/json - Return appropriate status codes: for creation,
201for deletion,204for reads/updates200 - Use path parameters with angle brackets:
shelf_router/<id>
- 每个资源对应一个处理函数类(UsersHandler、ProductsHandler)
- 处理函数方法代码行数不超过20行,业务逻辑全部委托给服务层
- 使用辅助方法为单个路由包裹认证中间件
_withAuth(handler) - 使用读取请求体,再通过
request.readAsString()解析jsonDecode() - JSON响应必须设置头
Content-Type: application/json - 返回合适的状态码:创建成功返回,删除成功返回
201,查询/更新成功返回204200 - 使用的尖括号语法定义路径参数:
shelf_router/<id>
Middleware
中间件(Middleware)
A is a function that takes a and returns a new .
MiddlewareHandlerHandlerdart
// Middleware type signature
typedef Middleware = Handler Function(Handler innerHandler);MiddlewareHandlerHandlerdart
// Middleware类型签名
typedef Middleware = Handler Function(Handler innerHandler);Writing Middleware
编写中间件
dart
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;
};
};
}dart
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
错误处理中间件
dart
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'},
);
}dart
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 (a function), not
MiddlewaredirectlyHandler - Always -- never skip calling the inner handler unless short-circuiting (auth failure, rate limit)
await innerHandler(request) - Use to pass data downstream
request.change(context: {...request.context, 'key': value}) - Error-handling middleware must catch all exceptions and return proper HTTP responses
- Never let unhandled exceptions propagate to
shelf_io.serve
- 返回(函数类型),不要直接返回
MiddlewareHandler - 始终,除非需要短路返回(认证失败、限流)否则不要跳过调用内部处理函数
await innerHandler(request) - 使用向下游传递数据
request.change(context: {...request.context, 'key': value}) - 错误处理中间件必须捕获所有异常,返回正确的HTTP响应
- 绝对不要让未处理的异常传播到层
shelf_io.serve
Request and Response
请求与响应
Reading Request Data
读取请求数据
| Source | How | Example |
|---|---|---|
| Body | | |
| Query params | | |
| Path params | Extra function arguments (shelf_router) | |
| Headers | | |
| Context | | |
| 来源 | 获取方式 | 示例 |
|---|---|---|
| 请求体 | | |
| 查询参数 | | |
| 路径参数 | 额外的函数参数(shelf_router提供) | |
| 请求头 | | |
| 上下文 | | |
Building Responses
构建响应
| Status | Method |
|---|---|
| 200 OK | |
| 201 Created | |
| 204 No Content | |
| 404 Not Found | |
| Modify response | |
| 状态码 | 用法 |
|---|---|
| 200 OK | |
| 201 Created | |
| 204 No Content | |
| 404 Not Found | |
| 修改响应 | |
Request/Response Rules
请求/响应规则
- Parse query parameters defensively with and defaults
int.tryParse - Read the body only once (it is a stream); do not call twice
readAsString() - Use for downstream data, never mutable globals
request.change(context:) - Set on every response that has a body
Content-Type
- 解析查询参数时做好防御性处理,使用和默认值
int.tryParse - 请求体只能读取一次(是流类型),不要重复调用
readAsString() - 使用向下游传递数据,绝对不要使用可变全局变量
request.change(context:) - 所有带响应体的返回都要设置头
Content-Type
Routing with shelf_router
使用shelf_router实现路由
dart
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);dart
final router = Router()
..get('/health', _health)
..get('/users', _listUsers)
..get('/users/<id>', _getUser)
..post('/users', _createUser)
..put('/users/<id>', _updateUser)
..delete('/users/<id>', _deleteUser);
// 带路径前缀挂载子路由
final root = Router()
..mount('/api/v1', apiRouter.call)
..mount('/ws', webSocketHandler);Routing Rules
路由规则
- Use cascade syntax for readability
..method('/path', handler) - 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
- 使用级联语法提升可读性
..method('/path', handler) - 使用挂载子路由
..mount('/prefix', router.call) - 路径参数使用尖括号定义:、
/<id>/<slug> - API路由必须加版本号:
/api/v1/... - 相关路由分组到同一个处理函数类中
Cascade (Fallback Routing)
Cascade(降级路由)
Cascadedart
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);Cascadedart
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
Cascade规则
- Place specific handlers (API) before generic handlers (static files)
- treats 404 and 405 as "not handled" by default
Cascade - Use parameter to customize which codes trigger fallthrough
statusCodes - Useful for SPAs: API routes first, then static file handler as fallback
- 具体的处理函数(API)放在通用处理函数(静态文件)前面
- 默认情况下会将404和405视为“未处理”,触发下一个处理函数
Cascade - 使用参数自定义触发降级的状态码
statusCodes - 非常适合SPA场景:先匹配API路由,静态文件处理作为降级兜底
Custom Exception Types
自定义异常类型
dart
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 {}]);
}dart
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 (not
Exception)Error - Map exceptions to HTTP status codes in error-handling middleware only
- Never throw generic -- use typed exceptions
Exception('message') - Include structured error details (field-level validation errors)
- 定义实现(不是
Exception)的领域异常Error - 仅在错误处理中间件中将异常映射为HTTP状态码
- 不要抛出泛型,使用带类型的异常
Exception('message') - 包含结构化的错误详情(字段级别的校验错误)
Commands
常用命令
bash
undefinedbash
undefinedDevelopment
开发
dart run bin/server.dart # Start server
dart run --enable-vm-service bin/server.dart # With debugger
dart run bin/server.dart # 启动服务
dart run --enable-vm-service bin/server.dart # 带调试器启动
Code generation (freezed, json_serializable)
代码生成(freezed、json_serializable)
dart run build_runner build --delete-conflicting-outputs
dart run build_runner watch # Watch mode for codegen
dart run build_runner build --delete-conflicting-outputs
dart run build_runner watch # 监听模式自动生成代码
Testing
测试
dart test # Run all tests
dart test test/handlers/ # Run specific directory
dart test --coverage # With coverage
dart test # 运行所有测试
dart test test/handlers/ # 运行指定目录的测试
dart test --coverage # 生成覆盖率报告
Quality
代码质量
dart format . # Format all files
dart analyze # Static analysis
dart fix --apply # Auto-fix lint issues
dart format . # 格式化所有文件
dart analyze # 静态代码分析
dart fix --apply # 自动修复lint问题
Build
构建
dart compile exe bin/server.dart -o server # AOT compile to native binary
undefineddart compile exe bin/server.dart -o server # AOT编译为原生二进制文件
undefinedBest Practices Summary
最佳实践总结
- Pipeline: Compose middleware with ; order matters (outermost runs first)
Pipeline - 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 directly in tests (no HTTP server needed); mock services with
shelfmocktail - Security: Validate all inputs; use parameterized database queries; never log secrets
- Performance: Compile to native executable for production; use streaming for large responses
- Pipeline:使用编排中间件,顺序很重要(最外层的中间件最先执行)
Pipeline - 处理函数:保持精简,逻辑委托给服务层,每个资源对应一个类
- 错误处理:在中间件中集中处理,捕获所有异常,返回结构化的JSON错误
- 上下文:通过在中间件和处理函数之间传递数据
request.change(context:) - 测试:测试中直接使用的API(不需要启动HTTP服务),使用
shelf模拟服务层mocktail - 安全:校验所有输入,使用参数化数据库查询,绝对不要打印日志输出密钥
- 性能:生产环境编译为原生可执行文件,大响应使用流式返回
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
如需详细的中间件示例、WebSocket支持、静态文件、认证流程、限流、测试模式,请参考:
- references/patterns.md -- 完整的中间件模式、WebSocket处理函数、静态文件服务、JWT认证、限流、仓库模式、集成测试、Dockerfile