本文属于 NestJS BBS 实战教程,共 11 节,建议按顺序学习。
1. 这节课要实现的功能是什么
- 把所有成功响应统一封装为
{ data, meta }。 - 把所有异常响应统一封装为
{ code, message, path, timestamp }。 - 让 API 风格稳定,便于前端和日志平台处理。
2. 这节课涉及的 NestJS 知识点有哪些(先讲概念)
2.1 Interceptor 与 Filter 的职责区别
- Interceptor:包裹正常执行流程,适合做结果转换、埋点、计时、脱敏;
- Exception Filter:捕获异常并统一序列化输出。
2.2 为什么这两个能力要全局注册
响应格式是跨模块规范,不应该让每个 Controller 手写。全局注册能保证一致性。
2.3 何时使用 APP_INTERCEPTOR / APP_FILTER
当能力是“全局横切”时用它们最合适,比如统一响应、统一错误、全局审计。
2.4 为什么 intercept() 返回的是 Observable
这是很多人第一次学 Interceptor 会卡住的点。
原因不是“Nest 特意要求你学 RxJS”,而是 Nest 把“后续执行结果”抽象成了一条可组合的响应流。
你可以这样理解:
next.handle()不是立刻拿到 Controller 的最终返回值;- 它拿到的是“Controller 后续执行结果的流”;
- Interceptor 可以在这条流上做统一加工。
所以 intercept() 返回 Observable,本质是在表达:
我不是直接返回一个最终值,而是在后续结果产生时,再对它做包装和转换。
2.5 Controller 的返回值为什么还会经过 Interceptor
Controller 并不是直接把结果写回给前端。更准确的执行顺序是:
- 请求先进入 Interceptor;
- Interceptor 调用
next.handle(); - Nest 执行 Controller 方法;
- Controller 返回原始结果;
- 结果回到 Interceptor;
- Interceptor 再把结果转换成统一格式;
- Nest 最后把处理后的结果写回 HTTP 响应。
所以 Interceptor 可以理解成“响应发给客户端前的最后一层加工通道”。
2.6 x-request-id 是谁提供的
当前项目里的 ResponseInterceptor 并不会自己生成 x-request-id,它只是读取请求头里已经存在的值:
- 前端主动传入;
- 网关或 Nginx 注入;
- 或者后端在更前面的 middleware 中补齐。
如果没有任何一层提供它,那么这里读到的就是 null。
2.7 有没有单独的 RequestInterceptor
没有。Nest 里只有一种 Interceptor,它天然就可以同时处理:
- 请求进入 Controller 之前的逻辑;
- Controller 返回结果之后的逻辑。
所以不是“框架里有一个 RequestInterceptor,再有一个 ResponseInterceptor”,而是同一个 intercept() 方法包裹了整个执行过程。
你可以这样理解:
next.handle()之前写的逻辑,属于“请求前”;next.handle().pipe(...)里写的逻辑,属于“响应后”。
只是当前项目里的这个 ResponseInterceptor 主要在做响应结果包装,所以名字看起来更像“响应拦截器”。
2.8 设计注意点
- Interceptor 不写业务规则;
- Filter 不吞掉异常语义,只负责输出格式;
- 格式稳定后,前端错误处理逻辑可以高度复用。
3. 实际开发中这些 NestJS 知识点被如何使用(详细代码)
3.1 src/common/interceptors/response.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';
@Injectable()
export class ResponseInterceptor<T>
implements NestInterceptor<T, { data: T; meta: { requestId: string | null } }> {
intercept(
context: ExecutionContext,
next: CallHandler<T>,
): Observable<{ data: T; meta: { requestId: string | null } }> {
const request = context.switchToHttp().getRequest<{ headers: Record<string, string | undefined> }>();
const requestId = request.headers['x-request-id'] ?? null;
return next.handle().pipe(
map((data) => ({
data,
meta: { requestId },
})),
);
}
}
这段代码可以按下面顺序理解:
- 先从请求头里尝试读取
x-request-id; - 调用
next.handle(),把后续 Controller 的执行结果接进来; - 用
pipe(map(...))把原始返回值统一包装成{ data, meta }; - 最终 Nest 再把包装后的结果输出给客户端。
如果 Controller 原本返回的是:
{ id: 1, username: 'tom' }
经过 Interceptor 后,真正返回给前端的就是:
{
data: { id: 1, username: 'tom' },
meta: { requestId: '...' }
}
3.2 src/common/filters/http-exception.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost): void {
const context = host.switchToHttp();
const response = context.getResponse();
const request = context.getRequest<{ url: string }>();
const statusCode =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
response.status(statusCode).json({
code: statusCode,
message: this.normalizeMessage(exception),
path: request.url,
timestamp: new Date().toISOString(),
});
}
private normalizeMessage(exception: unknown): string | string[] {
if (!(exception instanceof HttpException)) return 'Internal server error';
const payload = exception.getResponse();
if (typeof payload === 'string') return payload;
if (typeof payload === 'object' && payload !== null && 'message' in payload) {
return (payload as { message?: string | string[] }).message ?? 'Unexpected error';
}
return 'Unexpected error';
}
}
3.3 src/app.module.ts - 全局注册
import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: ResponseInterceptor,
},
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
3.4 本节落地价值
你不再依赖“每个人都记得按同样格式返回”,而是把接口规范固化成框架层能力。
4. 总结这节课知识点
- 你掌握了 Nest 的横切能力:Interceptor 与 Filter。
- 你实现了全局一致的成功/失败响应结构。
- 你为后续日志平台、前端 SDK、监控系统打下统一协议基础。
5. 每节课分支
lesson-08-interceptor-filter- 远程仓库:
https://github.com/Shiuwn/nestjs-tutorial.git