1. 为什么需要动态 Swagger 参数文档
在 NestJS 项目中,Swagger 文档是前后端协作的重要桥梁。传统静态文档的痛点在于:当接口参数需要根据业务场景动态变化时(比如不同用户角色看到不同字段),维护文档与代码的一致性会变成噩梦。我曾接手过一个电商后台项目,商品查询接口有 12 种条件组合,手动维护 Swagger 描述导致每周都要处理文档与实现不一致的报错。
动态文档的核心价值在于:
- 参数级实时同步:文档随业务逻辑自动更新,比如当权限系统新增"VIP用户"角色时,相关接口会自动展示VIP专属字段
- 条件化文档展示:同一个/users接口,管理员看到手机号字段而普通用户看不到
- 减少维护成本:项目迭代时不再需要同步修改文档注解
2. 动态文档的三种实现方案对比
2.1 装饰器重写方案
通过继承重写 @ApiProperty 装饰器,在运行时动态修改属性描述。这是我们团队最终采用的方案,优势在于:
typescript复制// 动态属性装饰器实现
export function DynamicApiProperty(options: (ctx: Request) => ApiPropertyOptions) {
return (target: any, propertyKey: string) => {
const request = Context.getCurrentRequest(); // 获取请求上下文
const resolvedOptions = options(request);
ApiProperty(resolvedOptions)(target, propertyKey);
};
}
// 在DTO中使用
class UserDTO {
@DynamicApiProperty((req) => ({
description: req.user.role === 'admin' ? '手机号(仅管理员可见)' : '隐藏字段'
}))
phone: string;
}
注意:需要确保装饰器在请求生命周期内执行,我们通过中间件在
Context中注入当前请求对象
2.2 Swagger Plugin 方案
利用 @nestjs/swagger 的插件系统,通过 DocumentBuilder 的 addPlugin 方法注入动态逻辑:
typescript复制const dynamicDescriptionPlugin = (document: OpenAPIObject) => {
document.paths['/users'].get.parameters = getUserVisibleParams();
};
// 在main.ts中
SwaggerModule.setup('api', app, document, {
swaggerOptions: {
plugins: [dynamicDescriptionPlugin]
}
});
适合全局性规则修改,但细粒度控制不如装饰器方案灵活。
2.3 中间件拦截方案
在 Swagger UI 请求时动态修改返回的 OpenAPI 规范:
typescript复制app.use('/api-docs', (req, res, next) => {
const originalJson = res.json;
res.json = function (data) {
data.paths = transformPaths(data.paths, req.user);
return originalJson.call(this, data);
};
next();
});
虽然实现简单,但会破坏 Swagger 的静态分析能力,不推荐生产环境使用。
3. 实战:基于装饰器的动态权限文档
3.1 搭建上下文管理系统
首先创建请求上下文服务,这是整个方案的基础:
typescript复制// context.service.ts
import { Request } from 'express';
export class Context {
private static requestMap = new WeakMap<object, Request>();
static setRequest(request: Request) {
this.requestMap.set(AsyncLocalStorage.getStore(), request);
}
static getCurrentRequest(): Request {
return this.requestMap.get(AsyncLocalStorage.getStore());
}
}
// context.middleware.ts
export function ContextMiddleware(req: Request, res: Response, next: NextFunction) {
const store = {};
AsyncLocalStorage.run(store, () => {
Context.setRequest(req);
next();
});
}
3.2 实现动态装饰器工厂
创建支持条件判断的动态装饰器生成器:
typescript复制// dynamic.decorators.ts
export function createDynamicDecorator(
decorator: MethodDecorator | PropertyDecorator,
resolver: (req: Request) => any
) {
return (target: any, key?: string | symbol, descriptor?: PropertyDescriptor) => {
const request = Context.getCurrentRequest();
if (!request) {
throw new Error('必须在请求上下文中使用动态装饰器');
}
const resolvedArgs = resolver(request);
if (descriptor) {
return decorator(target, key, descriptor, resolvedArgs);
}
return decorator(target, key, resolvedArgs);
};
}
// 使用示例
export const DynamicApiOperation = (optionsResolver: (req: Request) => ApiOperationOptions) =>
createDynamicDecorator(ApiOperation, optionsResolver);
3.3 权限敏感字段处理
针对不同角色展示不同字段描述:
typescript复制class ProductDTO {
@createDynamicDecorator(ApiProperty, (req) => ({
description: req.user.role === 'supplier'
? '成本价(供应商可见)'
: '该字段无权限查看',
type: Number,
required: false
}))
costPrice: number;
@DynamicApiResponse({
description: (req) => req.user.role === 'admin'
? '完整库存信息'
: '基础库存信息',
type: InventoryInfo
})
inventory: InventoryInfo;
}
4. 性能优化与生产实践
4.1 文档缓存策略
动态文档会带来性能开销,我们采用三级缓存:
- 内存缓存:对相同权限角色的请求缓存5分钟
- Redis缓存:集群环境下共享文档版本
- ETag协商缓存:客户端缓存校验
typescript复制// caching.interceptor.ts
@Injectable()
export class SwaggerCacheInterceptor implements NestInterceptor {
constructor(private readonly cacheManager: Cache) {}
async intercept(context: ExecutionContext, next: CallHandler) {
const request = context.switchToHttp().getRequest();
const cacheKey = `swagger:${request.user.role}`;
const cached = await this.cacheManager.get(cacheKey);
if (cached) {
return of(cached);
}
return next.handle().pipe(
tap((response) => {
this.cacheManager.set(cacheKey, response, { ttl: 300 });
})
);
}
}
4.2 文档生成性能监控
在 main.ts 中添加性能埋点:
typescript复制const document = SwaggerModule.createDocument(app, config);
console.time('Swagger generation');
// 添加动态处理逻辑
applyDynamicTransformations(document);
console.timeEnd('Swagger generation');
// 生产环境建议使用Metrics SDK上报到监控系统
5. 常见问题排查指南
5.1 装饰器不生效检查清单
- 上下文丢失:确认已注册
ContextMiddleware - 执行顺序问题:动态装饰器必须在
@ApiProperty之前执行 - 作用域错误:不要在
@Injectable()服务类中使用
5.2 文档生成性能问题
当文档生成超过1秒时:
- 检查是否在循环中调用了动态解析
- 验证Redis缓存是否正常工作
- 使用
--prof参数启动分析CPU占用
5.3 类型定义冲突
遇到TS类型检查报错时:
typescript复制// 使用类型断言解决动态类型问题
@DynamicApiOperation((req) => ({
description: '动态操作'
} as ApiOperationOptions))
async getProfile() {}
6. 进阶:动态文档的自动化测试
6.1 测试工具封装
创建测试专用上下文模拟器:
typescript复制export async function withMockedContext(
context: Partial<Request>,
handler: () => Promise<any>
) {
const store = {};
return AsyncLocalStorage.run(store, async () => {
Context.setRequest(context as Request);
return handler();
});
}
// 测试用例示例
it('should show admin fields', () => {
await withMockedContext({ user: { role: 'admin' } }, async () => {
const doc = generateDocument(AdminController);
expect(doc.paths['/users'].get.parameters).toContain('phone');
});
});
6.2 文档一致性校验
在CI流水线中添加文档校验:
bash复制# 在测试脚本中
npm run test:docs -- --validate
这个自定义命令会:
- 生成当前API文档
- 与上次提交的文档快照对比
- 如果存在非预期的差异则报错
7. 生产环境部署建议
7.1 安全注意事项
- 禁用Swagger UI的生产环境写入操作:
typescript复制SwaggerModule.setup('api', app, document, {
swaggerOptions: {
supportedSubmitMethods: ['get'] // 只允许GET
}
});
- 对文档端点添加权限控制:
typescript复制app.use('/api', (req, res, next) => {
if (!req.user.isInternal) {
return res.status(403).send('Forbidden');
}
next();
});
7.2 监控指标建议
建议监控以下指标:
| 指标名称 | 告警阈值 | 监控方式 |
|---|---|---|
| 文档生成时间 | >800ms | Percentile 95 |
| 动态装饰器执行次数 | >1000/s | Rate计数 |
| 文档缓存命中率 | <90% | 5分钟滚动窗口 |
8. 与其他工具的集成实践
8.1 与Prisma结合
当使用Prisma作为ORM时,可以从模型自动生成基础文档:
typescript复制function generateSwaggerFromPrisma(model: PrismaModel) {
return Object.entries(model.fields).map(([name, field]) => {
return {
name,
type: mapPrismaTypeToSwagger(field.type),
required: !field.isNullable
};
});
}
// 在DTO中使用
class UserDTO {
@ApiProperty({
description: '动态扩展字段',
required: false
})
@DynamicApiProperty((req) => ({
description: req.user.canSeeSensitive ? '真实姓名' : '名称'
}))
name: string;
...generateSwaggerFromPrisma(prisma.user)
}
8.2 与状态机集成
对于复杂业务流程,可以关联状态文档:
typescript复制@ApiExtraModels(OrderStateMachine)
class OrderDTO {
@DynamicApiProperty((req) => ({
description: '当前可执行操作',
enum: getAvailableActions(req.user.role)
}))
actions: string[];
}
9. 性能关键点实测数据
在我们的电商项目中进行压测(100并发):
| 方案 | 平均耗时 | 内存占用 | 适用场景 |
|---|---|---|---|
| 纯静态文档 | 23ms | 42MB | 简单API |
| 基础动态装饰器 | 156ms | 58MB | 大多数业务场景 |
| 全动态插件方案 | 210ms | 65MB | 需要深度定制文档 |
| 缓存优化后的方案 | 47ms | 45MB | 生产环境推荐 |
实测发现,在启用Redis缓存后,动态文档的性能损耗可以控制在静态文档的2倍以内,而带来的维护效率提升使得这个代价非常值得。