白留白WHITE SPACE
返回所有文章
NestJS / 后端开发

NestJS 实战 08:Interceptor 与 Exception Filter 统一响应风格

把所有成功响应统一封装为 { data, meta }。把所有异常响应统一封装为 { code, message, path, timestamp }。让 API 风格稳定,便于前端和日志平台处理。

本文属于 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 并不是直接把结果写回给前端。更准确的执行顺序是:

  1. 请求先进入 Interceptor;
  2. Interceptor 调用 next.handle();
  3. Nest 执行 Controller 方法;
  4. Controller 返回原始结果;
  5. 结果回到 Interceptor;
  6. Interceptor 再把结果转换成统一格式;
  7. 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 },
      })),
    );
  }
}

这段代码可以按下面顺序理解:

  1. 先从请求头里尝试读取 x-request-id;
  2. 调用 next.handle(),把后续 Controller 的执行结果接进来;
  3. 用 pipe(map(...)) 把原始返回值统一包装成 { data, meta };
  4. 最终 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

返回课程目录

上一课:BBS 核心域模型(板块、主题、回复)

下一课:Swagger 文档、限流与缓存

— 感谢阅读 —