fastify-wrap
v0.1.0
Published
A thin wrapper on top of Fastify — decorator-based routing, JWT auth, HMAC signature, JSON Schema validation, DB connection pooling, auto-scanned controllers.
Readme
fastify-api
Fastify 之上的一层薄封装。开发者只需写 controller / service / dao,路由注册、JWT 鉴权、JSON Schema 校验、统一异常处理、DB 连接池全部自动处理。
核心理念:极简。不包装 Happy Path,不做 { code, data, message } 统一响应格式。Controller 返回什么就是什么,异常由框架统一捕获。
安装
npm install fastify-wrap快速开始
1. 入口文件
// src/main.ts
import { createApp } from 'fastify-wrap';
import config from './config';
const app = await createApp(config);
await app.listen({ port: 3000 });// src/config.ts
import type { AppConfig } from 'fastify-wrap';
import { createDbSecretResolver } from 'fastify-wrap';
const config: AppConfig = {
db: {
host: '127.0.0.1',
port: 3306,
user: 'root',
password: 'root',
database: 'mydb',
},
auth: {
jwtSecret: 'your-secret',
},
signature: {
resolveSecret: createDbSecretResolver(),
},
};
export default config;多环境部署详见多环境配置。
2. 写 Controller
// src/modules/user/controller/UserController.ts
import { Controller, Get, Post, Public, Login, Valid } from 'fastify-wrap';
import type { FastifyRequest } from 'fastify';
@Controller('/api/users')
export class UserController {
@Post('/')
@Public()
@Valid({
body: {
type: 'object',
required: ['name'],
properties: { name: { type: 'string', minLength: 1 } },
},
})
async create(request: FastifyRequest) {
// request.body 已通过 JSON Schema 校验
return { id: '1', name: request.body.name };
}
@Get('/me')
@Login()
async profile(request: FastifyRequest) {
// request.user 已注入(JWT 鉴权通过后)
return request.user;
}
@Get('/admin')
@Login('admin')
async adminOnly() {
return { secret: 42 };
}
}
// Controller 只需 request,无需 reply:
// - 返回值自动序列化为 JSON
// - JWT 签名用 signToken() 全局函数
// - 文件下载用 @Stream 装饰器文件放到约定目录即自动发现,无需手动注册路由。
3. RPC 风格 Controller
加 @Rpc(name) 将类切换为 RPC 风格。无需 @Controller,无需在每个方法上加 @Post——框架自动扫描所有 public 方法,URL 由模块目录 + 控制器名 + 方法名自动推导。
import { Rpc, Login, Body, signToken } from 'fastify-wrap';
import type { User } from 'fastify-wrap';
import { Type } from '@sinclair/typebox';
@Rpc('users') // 控制器名,URL: /{module}/users/*
@Login() // 类级别:所有方法默认需要 JWT(可被方法级 @Login/@Public 覆盖)
export class UsersController {
// POST /{module}/users/create
@Body(Type.Object({ name: Type.String() }))
@Response('创建用户', Type.Object({ id: Type.String() }))
create(body: { name: string }, user: User): { id: string } {
return { id: 'u_001' };
}
// POST /{module}/users/login — public 端点,覆盖类级 @Login
@Public()
@Body(Type.Object({ code: Type.String() }))
@Response('登录', Type.Object({ token: Type.String() }))
async login(body: { code: string }): Promise<{ token: string }> {
const token = await signToken({ id: 'openid_xxx' });
return { token };
}
// POST /{module}/users/profile — 继承类级 @Login
@Response('获取用户信息', Type.Object({ id: Type.String() }))
profile(_body: Record<string, never>, user: User): { id: string } {
return { id: user.id };
}
}RPC 方法签名 (body, user):第一个参数是已校验的请求体,第二个参数是 request.user。
路由推导规则:
| 来源 | 推导逻辑 |
|------|----------|
| module | 模块文件夹名,可通过 route.config.ts 覆盖 |
| controller | @Rpc(name) 参数,默认为类名 |
| method | 方法名,保持不变 |
// src/modules/user/route.config.ts
export const module = 'u';
// → URL: /u/{controller}/{method}urlPrefix 是完整的路径前缀段,支持多级路径。 可以包含 / 构造嵌套路由:
// src/modules/user-rpc/route.config.ts
export const urlPrefix = 'api/urpc';
// → URL: /api/urpc/{controller}/{method}不配置时 URL 前缀默认使用模块目录名。仅在 @Rpc 控制器中生效,@Controller 不受影响。
模块级 token 隔离:route.config.ts 可配置 module,校验 token 的 user.type,防止跨模块 token 滥用:
// src/modules/admin/route.config.ts
export const urlPrefix = 'api/admin'; // URL 路径(可选,默认使用模块目录名)
export const module = 'admin'; // 模块归属,所有 @Rpc 路由要求 user.type === 'admin'@Login() 的参数与模块配置的 module 是同一概念——两者都声明"此路由需要什么系统的 token",区别仅在于作用范围。优先级:方法级 > 类级 > 模块级:
| 配置 | 校验 | 作用范围 |
|------|------|----------|
| @Login('admin')(方法上) | HMAC + JWT + user.type === 'admin' | 单个方法 |
| @Login('admin')(类上) | 同上 | 整个 Controller,可被方法级覆盖 |
| 模块 module:'admin' | 同上 | 整个模块,可被类/方法级覆盖 |
| @Login()(无参数) | HMAC + JWT(不校验 type) | 方法/类 |
| @Public() | 跳过所有 | 单个方法 |
| 限制 | 说明 |
|------|------|
| HTTP 方法 | 全部使用 @Post,不可更改 |
| 参数 | 只用 @Body,不支持 @Query/@Params |
| URL | 不支持路径参数 :id |
4. contextPath
部署在反向代理子路径下时,配置 contextPath,Controller 代码无需变动:
const app = await createApp({
db: { ... },
contextPath: '/myapp',
});
// @Controller('/api/users') → 响应地址:/myapp/api/users搭配 nginx:
location /myapp/ {
proxy_pass http://127.0.0.1:3000;
}配置项
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| db | object | 是 | MySQL 连接(host / port / user / password / database / connectionLimit) |
| auth.jwtSecret | string | 否 | JWT 密钥,不设则跳过鉴权 |
| signature | object | 否 | HMAC 签名鉴权配置(resolveSecret / timestampWindowMs / nonceStore) |
| redis | object | 否 | Redis 连接(host / port / password / db),配置后签名 nonce 自动切换 Redis 存储 |
| contextPath | string | 否 | 路径前缀,如 /myapp,Controller 代码无需变动 |
| swagger | object | 否 | Swagger / OpenAPI 文档配置(title / description / version / uiPrefix) |
| migrate | boolean | 否 | 启动时执行 src/migrations/*.sql,默认 false |
多环境配置
源码保留 3 份环境配置文件作为参考,构建时只复制目标那份为 config.ts 参与编译。
目录结构
src/
├── main.ts
├── config.ts ← 构建时由脚本写入(.gitignore)
└── config/
├── development.ts ← 源码参考:本地开发
├── test.ts ← 源码参考:测试环境
└── production.ts ← 源码参考:生产环境构建脚本
{
"scripts": {
"build:dev": "copy src\\config\\development.ts src\\config.ts >nul 2>&1 & npx tsc",
"build:test": "copy src\\config\\test.ts src\\config.ts >nul 2>&1 & npx tsc",
"build:prod": "copy src\\config\\production.ts src\\config.ts >nul 2>&1 & npx tsc"
}
}配置文件写法
// src/config/production.ts
import type { AppConfig } from 'fastify-wrap';
const config: AppConfig = {
db: {
host: process.env.DB_HOST!, // 敏感信息从环境变量注入
port: Number(process.env.DB_PORT),
user: process.env.DB_USER!,
password: process.env.DB_PASSWORD!,
database: process.env.DB_NAME!,
connectionLimit: 20, // 生产环境提高连接数
},
auth: {
jwtSecret: process.env.JWT_SECRET!,
},
swagger: false, // 生产环境关闭文档
migrate: false, // 生产环境不在应用进程内执行迁移
};
export default config;入口文件
// src/main.ts
import { createApp } from 'fastify-wrap';
import config from './config';
const app = await createApp(config);
await app.listen({ port: 3000 });Docker 部署
FROM node:24-alpine
WORKDIR /app
COPY . .
RUN npm install && npm run build:prod
CMD ["node", "dist/main.js"]敏感信息通过环境变量注入,不写在配置文件里:
# 测试环境(生产用 build:prod,方法同上)
docker run -d \
-e DB_HOST=192.168.1.10 \
-e DB_PORT=3306 \
-e DB_USER=app \
-e DB_PASSWORD=test123 \
-e DB_NAME=mydb_test \
-e JWT_SECRET=test-secret \
my-app:latest项目结构
src/
├── main.ts
└── modules/
└── user/
├── controller/UserController.ts ← @Controller/@Get/@Post 声明路由
├── service/UserService.ts ← 纯业务流程编排
├── dao/
│ ├── UserDao.ts ← db.query/db.queryOne/db.execute
│ └── UserDao.sql.ts ← SQL 常量(禁止内联 SQL)
├── dto/
│ ├── CreateUserRequest.ts
│ └── UserResponse.ts
├── entity/UserEntity.ts
└── enums/UserStatus.ts装饰器
| 装饰器 | 作用 |
|--------|------|
| @Controller('/prefix') | 声明路由前缀,类必须 export |
| @Get('/path') | GET 路由 |
| @Post('/path') | POST 路由 |
| @Put('/path') | PUT 路由 |
| @Delete('/path') | DELETE 路由 |
| @Login() | 需要 JWT + 签名验证。支持类级别(作用于所有方法)和方法级别(覆盖类级设置) |
| @Login('admin') | 需要 JWT + 校验 user.type === 'admin' + 签名验证。支持类级别和方法级别 |
| @Public() | 跳过签名验证(公开接口) |
| @Valid(schema) | 绑定 Fastify JSON Schema 校验 |
| @Body(def) | 声明式请求体校验 |
| @Query(def) | 声明式查询参数校验 |
| @Params(def) | 声明式路径参数校验 |
| @Response(description, def, statusCode?) | 声明响应 Schema,用于 OpenAPI 文档生成,默认 200 |
| @Rpc(name?) | RPC 风格控制器。无需 @Controller,方法自动扫描,签名 (body, user)。name 覆盖控制器名,默认类名。 |
| @Stream(description?) | 流式下载端点,方法返回 StreamResponse { name, content }。description 注入 OpenAPI 响应描述。 |
| @File(description?) | 文件下载端点,方法返回 FileResponse { name, path }。description 注入 OpenAPI 响应描述。 |
| @Multipart() | 标记 multipart/form-data 文件上传端点。替代 @Body(输入层),仍需 @Response(输出层)。 |
声明式校验
比手写 JSON Schema 更简洁的类型工具:
import {
Body, Query, Params,
String, Number, Boolean, Email, Required, ArrayOf, Enum, Min, Max,
} from 'fastify-wrap';
@Controller('/api/users')
class UserController {
@Post('/')
@Body({
name: Required(String),
email: Required(Email),
age: Number,
tags: ArrayOf(String),
status: Enum(['active', 'inactive']),
})
create(request: FastifyRequest) {
// request.body 已校验
}
@Get('/')
@Query({ page: Number, keyword: String })
list(request: FastifyRequest) { ... }
@Get('/:id')
@Params({ id: Required(String) })
show(request: FastifyRequest) { ... }
}@Body/@Query/@Params 自动生成标准 JSON Schema,与 @Valid 等价但可读性更高。
Swagger / OpenAPI
配置 swagger 即可自动生成 OpenAPI 文档,路由、参数、Schema 全部自动提取:
const app = await createApp({
db: { ... },
swagger: {
title: 'My API',
description: 'API 文档',
version: '1.0.0', // 可选,默认读取 package.json version
uiPrefix: '/docs', // 可选,默认 /docs
},
});启动后访问 /docs 即可浏览 Swagger UI,/docs/json 获取 OpenAPI JSON。
配置 contextPath 时自动联动,例如 contextPath: '/myapp' → Swagger UI 在 /myapp/docs。
依赖自动补充 @fastify/swagger + @fastify/swagger-ui。
统一响应
Happy path:Controller return data,原样输出 JSON,不做包装。
{ "id": "1", "name": "张三" }Error path:自动捕获异常,统一格式:
HTTP 401 ← JWT 鉴权失败 / HMAC 签名失败
HTTP 403 ← 角色权限不足
HTTP 404 ← 路由不存在
HTTP 422 ← BusinessException、NotFoundException、Schema 校验失败
HTTP 500 ← ServiceException、未预期异常(message 通用化,不暴露堆栈)业务/校验异常:
{ "error": "手机号已存在" }鉴权异常:
{ "code": "login", "error": "Authorization token is invalid" }
{ "code": "error", "error": "签名验证失败" }
{ "code": "retry", "error": "凭证无效或已过期" }异常
import { BusinessException, NotFoundException, ServiceException } from 'fastify-wrap';
throw new BusinessException('库存不足'); // → 422 { error: '库存不足' }
throw new NotFoundException('用户不存在'); // → 422 { error: '用户不存在' }
throw new ServiceException('推送失败'); // → 500 { error: '服务器内部错误' }API 签名鉴权
所有接口默认需要 HMAC 签名验证。@Public() 标记的接口跳过签名,@Login() 标记的接口在签名基础上额外要求 JWT 登录。
签名验证按 module_name 隔离——admin 的 clientId 无法调用其他模块的接口。
鉴权模型
1. 线下派发 clientId + secretKey
2. 客户端每次请求计算 HMAC-SHA256 签名
3. 服务端验证时间戳、nonce、签名,按 module_name 过滤客户端签名算法
stringToSign = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + BODY
signature = base64(HMAC-SHA256(secretKey, stringToSign))请求头:
| Header | 说明 |
|--------|------|
| X-App-Key | 客户端 ID |
| X-Timestamp | 毫秒时间戳 |
| X-Nonce | 随机 UUID |
| X-Signature | 签名结果 |
服务端配置
框架内置两种 SecretResolver 驱动:
import { createDbSecretResolver, createRedisSecretResolver } from 'fastify-wrap';
// DB 驱动 — 从 MySQL 的 api_keys 表查签名密钥
createDbSecretResolver();
// Redis 驱动 — 从 Redis 查 clientId,值存 JSON
// Redis key = `${keyPrefix}${moduleName}:${clientId}`(不同模块隔离)
createRedisSecretResolver({
keyPrefix: 'client:secret:', // 默认 'client:secret:'
});配置示例:
const app = await createApp({
db: { ... },
signature: {
resolveSecret: createDbSecretResolver(),
timestampWindowMs: 5 * 60 * 1000, // 可选,默认 5 分钟
},
});也可自定义 resolveSecret 函数对接任意存储。
路由使用
默认所有接口需签名,request.user.id 为 clientId:
@Controller('/api/app')
export class AppController {
@Get('/data')
async data(request: FastifyRequest) {
return { clientId: request.user.id };
}
@Get('/profile')
@Login()
async profile(request: FastifyRequest) {
// 签名通过 + JWT 登录通过 → request.user 为 JWT 用户
return request.user;
}
}签名失败统一返回 401 { code: 'error' | 'retry', error: string }:
code: 'error'— 不可重试(签名不匹配、缺少参数、timestamp/nonce 异常)code: 'retry'— 凭证无效或过期,客户端刷新凭证后重试
服务端日志会详细记录失败原因和客户端数据。支持 nonce 防重放(内存或 Redis,自动切换)、timestamp 时间窗口校验、constant-time 签名比对。
文件上传
上传时签名逻辑不变:将 JSON 元数据签名后以 _sign_body 字段嵌入 multipart/form-data,文件以 file 字段附带:
@Controller('/api/app')
class AppController {
@Post('/upload')
async upload(request: FastifyRequest) {
const body = request.body as Record<string, unknown>;
// body.file → { type: 'file', fieldname, filename, mimetype, file, _buf }
// body._sign_body → { type: 'field', value: '{"fileName":"a.png",...}' }
}
}文件下载
import { Controller, Get, Stream } from 'fastify-wrap';
import type { StreamResponse } from 'fastify-wrap';
@Controller('/api/report')
class ReportController {
@Get('/export')
@Stream('导出CSV报表')
async exportCSV(): Promise<StreamResponse> {
const csv = ['name,phone', '张三,138xxxx'].join('\n');
return { name: 'report.csv', content: csv };
}
}Controller 只需返回 { name, content },框架自动设置 Content-Disposition 并流式返回。无需 reply 参数。
content 支持 string 和 Buffer,适用 CSV / Excel / 图片等任意二进制数据。
JWT 工具
signToken — 全局 JWT 签名
import { signToken } from 'fastify-wrap';
const token = await signToken({ id: 1, role: 'admin', type: 'admin' });全局可用,无需传递 reply 对象。在 createApp() 初始化 JWT 插件后自动生效。
DB 工具
import { db } from 'fastify-wrap';
// 查多行
const users = await db.query<UserRow>('SELECT * FROM users WHERE status = ?', [0]);
// 查单行
const user = await db.queryOne<UserRow>('SELECT * FROM users WHERE id = ?', [id]);
// 增删改
await db.execute('UPDATE users SET name = ? WHERE id = ?', ['李四', id]);无需手动 getConnection / release,自动管理连接池。
事务
import { db, Transaction } from 'fastify-wrap';
await db.transaction(async (tx: Transaction) => {
await tx.execute('UPDATE account SET balance = balance - 100 WHERE id = ?', [1]);
await tx.execute('UPDATE account SET balance = balance + 100 WHERE id = ?', [2]);
});
// 成功自动 commit,异常自动 rollback + release事务内使用 tx.query / tx.queryOne / tx.execute,接口与 db 一致。
Redis 工具
import { redis } from 'fastify-wrap';
await redis.set('key', 'value', 60); // 带 TTL(秒)
const val = await redis.get('key'); // → 'value' | null
await redis.del('key');
const exists = await redis.exists('key'); // → boolean
// 原子 SET NX EX(用于分布式锁/防重放)
const ok = await redis.setnx('lock:order:123', '1', 30);配置:
createApp({
db: { ... },
redis: {
host: '127.0.0.1',
port: 6379,
password: 'optional',
db: 0,
},
});配置 Redis 后,签名验证的 nonce 防重放自动切换为 RedisNonceStore(跨实例安全)。不配 Redis 则用内存 MemoryNonceStore。
也可显式传入自定义 NonceStore:
import { SignatureStrategy, RedisNonceStore } from 'fastify-wrap';
const strategy = new SignatureStrategy({
resolveSecret,
nonceStore: new RedisNonceStore(),
});分页
import type { PageResult } from 'fastify-wrap';
@Get('/')
async list(): Promise<PageResult<UserResponse>> {
return {
items: [...],
total: 100,
page: 1,
pageSize: 20,
};
}枚举规范
// DB number → 数值枚举
export enum UserStatus {
NORMAL, // 0
LOCKED, // 1
}
// DB string → 字符串枚举
export enum OrderType {
NORMAL = 'NORMAL',
RUSH = 'RUSH',
}所见即所得,无额外 code/description 字段。
二次开发
框架设计为可扩展,以下是在其之上做二次开发的常见模式。
定制校验错误消息
框架内置 errorRules,覆盖 24 个 JSON Schema 关键字的默认中文提示。可通过 addRule 覆盖:
import { addRule } from 'fastify-wrap';
// 全局生效,框架启动前调用一次即可
addRule('required', (field) => `${field} 是必填的`);
addRule('pattern', (field) => `${field} 格式不对`);
addRule('type', (field) => `${field} 类型不匹配`);不调用则使用内置默认(password 必填、phone 格式错误、age 类型错误 等)。未覆盖的关键字回退到 AJV 原始英文。
完整的关键字列表见 src/common/validation/errorRules.ts。
自定义 AJV 校验关键字
标准 JSON Schema 关键字不够用时,通过 addKeyword 注册自己的规则:
// src/main.ts — createApp 之前调用
import Ajv from 'ajv';
const ajv = new Ajv();
ajv.addKeyword({
keyword: 'isPhone',
validate: (_schema, data) => /^1[0-9]{10}$/.test(data),
errors: true,
error: {
message: (cxt) => `${cxt.instancePath.replace(/^\//, '')} 手机号格式不正确`,
},
});然后在 Controller 中使用:
// Typebox 风格
const Request = Type.Object({
phone: Type.String({ isPhone: true }),
});
// SchemaProp 风格
@Body({ phone: { type: 'string', isPhone: true } })关键字注册时自带的 error.message 会自动被 errorRules.formatError 使用,无需额外配置。
自定义 SchemaProp 辅助
在 schema.ts 中新增类型辅助,在项目中任何 Controller 直接使用:
// src/common/validation/schema.ts
export const Phone: SchemaProp = { type: 'string', pattern: '^1[0-9]{10}$' };
export const IdCard: SchemaProp = {
type: 'string',
pattern: '^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$',
};// Controller
@Body({
phone: Required(Phone),
idCard: Required(IdCard),
})自定义签名 SecretResolver
对接任何存储系统:
createApp({
signature: {
resolveSecret: async (clientId: string, moduleName: string) => {
const row = await db.queryOne(
'SELECT secret_key FROM api_keys WHERE client_id = ? AND module_name = ?',
[clientId, moduleName],
);
return row
? { appSecret: row.secret_key as string }
: null;
},
},
});自定义 NonceStore
签名防重放实现可替换:
import { SignatureStrategy } from 'fastify-wrap';
class MyNonceStore {
async exists(nonce: string): Promise<boolean> { /* ... */ }
async mark(nonce: string, ttlMs: number): Promise<void> { /* ... */ }
async clear(/* ... */): Promise<void> { /* ... */ }
}
const strategy = new SignatureStrategy({
resolveSecret,
nonceStore: new MyNonceStore(),
});自定义 LoginStrategy
对接非 JWT 的登录鉴权(Session、OAuth、自定义 Token):
import type { AuthStrategy, User } from 'fastify-wrap';
class SessionStrategy implements AuthStrategy {
async authenticate(request: FastifyRequest, _reply: FastifyReply): Promise<User> {
const sessionId = request.cookies.session;
const user = await db.queryOne('SELECT * FROM sessions WHERE id = ?', [sessionId]);
if (!user) throw new AuthException('会话已过期');
return { id: user.id, role: user.role, type: 'session' };
}
}自定义 Controller 元数据扫描
框架通过 Reflect.defineMetadata / getMetadata 管理路由元数据。装饰器和扫描逻辑在 src/common/scanner/ 中,侵入式修改可参考:
decorators.ts— 新增装饰器(如@RateLimit、@Cache)index.ts— 扫描和路由注册(scanControllers)
框架内置的 @Response、@Stream 均按此模式实现,是添加新装饰器的范本。
新增装饰器示例:
const RATE_LIMIT_META = Symbol('route:rateLimit');
export function RateLimit(max: number, windowMs: number): MethodDecorator {
return (target, propertyKey) => {
Reflect.defineMetadata(RATE_LIMIT_META, { max, windowMs }, target, propertyKey);
};
}// scanner/index.ts 中读取
const rateLimitMeta = Reflect.getMetadata(RATE_LIMIT_META, exported, route.handlerName);
if (rateLimitMeta) {
routeDef.preHandler = [/* rate limiter hook */];
}自测客户端
框架内置 Node.js 端 HMAC 签名客户端,无需浏览器或额外依赖,可直接用于集成测试:
import { TestClient, ApiError, IOException } from 'fastify-wrap/client';
const client = TestClient.withCredential(
'http://127.0.0.1:3001',
'app-demo-001',
'sk-your-secret',
);
// JSON 请求 — 成功返回 body,失败抛 ApiError
const profile = await client.get<{ appKey: string }>('/api/app/profile');
const echo = await client.post<{ appKey: string }>('/api/app/echo', { msg: 'hello' });
// 文件上传
const uploadRes = await client.upload<{ file: { name: string } }>(
'/api/app/upload',
{ fileName: 'test.png', type: 'image/png', size: 11 },
{ data: Buffer.from('...'), filename: 'test.png', mimetype: 'image/png' },
);
// 文件下载
const blobRes = await client.blob('/api/app/download');
const text: string = await blobRes.body.text();
// blobRes.filename — 从 Content-Disposition 解析
// POST 下载(带请求体的文件下载)
const blobRes2 = await client.postBlob('/api/app/export', { format: 'csv' });
// 错误处理
try {
await client.get('/api/secret');
} catch (e) {
if (e instanceof ApiError) {
console.log(e.statusCode, e.code, e.message);
// statusCode: 422, code: undefined, message: "..."
}
if (e instanceof IOException) {
console.log('Network error');
}
}复杂配置模式:
const client = new TestClient({
baseUrl: 'http://127.0.0.1:3001',
getCredential: () => ({ appKey: 'k', appSecret: 's' }),
refreshCredential: async (appKey) => {
// 401 code=retry 时自动调用,传入当前 appKey
return { appKey: 'k2', appSecret: 's2' };
},
getToken: () => process.env.JWT_TOKEN ?? null, // JWT Bearer token
onLoginRequired: () => { /* 401 code=login 时跳转登录 */ },
skipAuthPaths: ['/api/auth/login'], // 跳过签名的路径
toast: (msg) => console.error(msg), // error='toast' 时触发
alert: (msg) => window.alert(msg), // error='alert' 时触发
logger: true, // 打印请求/响应日志
});
// 每个请求可单独控制错误处理
await client.get('/api/resource'); // 默认 toast
await client.get('/api/resource', 'alert'); // alert 弹窗
await client.get('/api/resource', (msg) => console.log(msg));// 自定义处理数据库迁移
配置 migrate: true 即可在启动时自动执行 src/migrations/ 下的 .sql 文件:
const app = await createApp({
db: { ... },
migrate: true,
});约定:
- SQL 文件放在
src/migrations/,按文件名字母序执行 - 框架自动创建
migrations追踪表,已执行的不会重复跑 - 无需手动登库执行 DDL
-- src/migrations/001_create_users.sql
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL
);依赖
- Fastify 5
- @fastify/jwt(JWT 鉴权)
- @fastify/multipart(文件上传)
- @fastify/mysql + mysql2(MySQL 连接池)
- @fastify/redis + ioredis(Redis,可选)
- @fastify/swagger + @fastify/swagger-ui(API 文档,可选)
- reflect-metadata(装饰器元数据)
- fast-glob(Controller 自动扫描)
TypeScript experimentalDecorators 和 emitDecoratorMetadata 必须启用。
参考
待实现
| 特性 | 说明 |
|------|------|
| CORS | @fastify/cors,跨域支持 |
| 安全头 | @fastify/helmet,CSP / HSTS / X-Frame-Options 等 |
| 请求 ID / 链路追踪 | 为每个请求注入 trace-id,日志可串联 |
| 限流 | 基于令牌桶或滑动窗口的速率限制 |
| 缓存抽象 | 防穿透(空值缓存)、防击穿(单飞锁)、防雪崩(随机 TTL) |
