NestJS中如何向拦截器传递变量或读取业务标记值
NestJS拦截器读取数据来源标记实现方案
需求背景
需要实现用户列表接口逻辑:
- 优先读取缓存返回数据,缓存未命中时从数据库读取
- 根据数据实际来源(缓存/数据库)设置对应响应头
- 响应头设置逻辑统一交由拦截器处理,不耦合在控制器/服务层业务逻辑中
现有代码
控制器代码
@UseInterceptors(CacheOrServiceHeaderInterceptor) @Get('users') public async users( @Query('id') clinicianId: number, @Query('name') specialtyName: string, ){ const setFlags: MetaFlags = {setCacheHeader: false, setServiceHeader: false}; const data = await this.service.getUsers(id, name, setFlags); return data; }
服务层代码
export interface MetaFlags { setServiceHeader: boolean setCacheHeader?: boolean, } // 省略类其他逻辑 public async getUsers(....){ // 内部逻辑判断走缓存还是数据库 // 命中缓存则设置 setCacheHeader = true // 走数据库查询则设置 setServiceHeader = true }
原有拦截器代码
@Injectable() export class CacheOrServiceHeaderInterceptor implements NestInterceptor { public intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next .handle() .pipe( map((data) => { const req = context.switchToHttp().getRequest(); // 此处需要拿到实际的标记值判断设置对应头 if(MetaFlags.setCacheHeader) // 设置缓存来源响应头 else // 设置数据库来源响应头 req.res.set('x-api-key', 'pretty secure'); return data; }) ); } }
实现方案
最简便无侵入的实现方式是利用HTTP请求对象在整个请求生命周期共享的特性,将标记位挂载到请求实例上传递,全程不需要修改接口返回的数据结构:
- 第一步:补全TS类型声明(JS项目可跳过这步),在项目任意
.d.ts文件中添加类型扩展,避免TS类型报错:
// 若使用Express作为HTTP底层 declare module 'express' { interface Request { sourceFlags?: MetaFlags } } // 若使用Fastify作为HTTP底层则替换为以下声明 // declare module 'fastify' { // interface FastifyRequest { // sourceFlags?: MetaFlags // } // }
- 第二步:修改控制器代码,注入请求对象,初始化标记位后挂载到请求实例上,再传入服务层:
@UseInterceptors(CacheOrServiceHeaderInterceptor) @Get('users') public async users( @Req() req: Request, // 注入请求对象 @Query('id') clinicianId: number, @Query('name') specialtyName: string, ){ // 初始化标记位直接挂载到请求对象上 req.sourceFlags = {setCacheHeader: false, setServiceHeader: false}; // 传入标记位,服务层修改对象属性时因为引用传递,会直接修改req上挂载的值 const data = await this.service.getUsers(id, name, req.sourceFlags); return data; }
- 第三步:服务层代码不需要做任何改动,原有修改传入
setFlags属性的逻辑可以直接复用。 - 第四步:修改拦截器逻辑,直接从请求对象上读取标记位判断设置响应头即可:
@Injectable() export class CacheOrServiceHeaderInterceptor implements NestInterceptor { public intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const req = context.switchToHttp().getRequest(); const res = context.switchToHttp().getResponse(); return next.handle().pipe( tap(() => { const flags = req.sourceFlags; if (flags?.setCacheHeader) { // 设置缓存来源对应的响应头,示例: res.set('X-Data-Source', 'cache'); } else if (flags?.setServiceHeader) { // 设置数据库来源对应的响应头,示例: res.set('X-Data-Source', 'database'); } // 其他公共响应头可以在这里统一设置 res.set('x-api-key', 'pretty secure'); }) ); } }
这里用
tap操作符替代map,因为不需要修改返回的响应数据,只需要做设响应头这类副作用操作,语义更准确,也没有额外的数据处理开销。
备选方案
如果不想在请求对象上挂载自定义属性,也可以调整服务层返回结构,将标记位和业务数据一起返回:
// 服务层返回结构调整为 return { data: 用户列表数据, meta: { setCacheHeader: true/false, setServiceHeader: true/false } }
拦截器中拿到返回值后,先读取meta中的标记位设置响应头,再把data字段返回给客户端即可。这种方式的缺点是需要修改服务层、控制器的返回逻辑,侵入性更高。
内容的提问来源于stack exchange,提问作者salt-pepper
相关产品推荐
相关产品推荐

