Skip to content

NestJS 全局响应与异常处理最佳实践

统一接口规范 · 防御式编程 · 提升前后端协作效率

1. 背景与目标

在 NestJS 应用中,若缺乏统一的响应格式和异常处理机制,容易导致:

  • 前端需兼容多种返回结构({ data }{ result }、HTML 错误页等)
  • 错误信息不明确(如仅返回 500 Internal Server Error
  • DTO 校验失败无法友好提示
  • 业务错误码散落在代码各处(“魔法数字”)

为此,我们通过 TransformInterceptor + AllExceptionsFilter 构建标准化的请求生命周期处理链:

请求 → DTO 校验 → Controller → 成功?→ TransformInterceptor(包装成功)  
                                 ↓ 否  
                          AllExceptionsFilter(统一错误)

2. 核心组件设计

2.1 TransformInterceptor:统一成功响应

功能

将所有正常返回值包装为标准结构:

json
{
  "code": 0,
  "message": "操作成功",
  "data": {}
}

推荐实现(安全版)

ts
// src/common/interceptors/transform.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) => ({
        code: 0,
        message: '操作成功',
        data, // ⚠️ 直接透传,不擅自替换 null/undefined
      })),
    );
  }
}

关键原则

原则说明
不篡改业务语义null 就是 null,前端应能正确处理;若需默认值,应在 Service/DTO 层处理
仅适用于 JSON 接口文件下载、SSE 等流式响应应使用 @Res() 并绕过全局拦截器
消息保持简洁统一避免动态 message,除非有强业务需求

若未来需支持多语言,可将 '操作成功' 提取为常量或通过 i18n 服务注入。

2.2 ✅ AllExceptionsFilter:统一异常兜底

功能

捕获所有异常(系统错误、DTO 校验失败、业务异常),返回结构化错误:

json
{
  "code": 400,
  "message": "参数错误: 用户名不能为空",
  "data": null,
  "timestamp": "2025-11-07T10:00:00.000Z",
  "path": "/api/user"
}

推荐实现

ts
// src/common/exceptions/all-exceptions.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { BusinessException } from './business.exception';
import { BUSINESS_ERROR_CODE } from './business.error.code';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly logger = new Logger(AllExceptionsFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    let code: number;
    let message: string;
    let status: number;

    // 1. 业务自定义异常
    if (exception instanceof BusinessException) {
      code = exception.getError().code;
      message = exception.getError().message;
      status = HttpStatus.OK; // 业务异常通常返回 200 + code≠0
    }
    // 2. HTTP 异常(含 DTO 校验失败)
    else if (exception instanceof HttpException) {
      const errorResponse = exception.getResponse();
      status = exception.getStatus();

      // 处理 ValidationPipe 抛出的 BadRequestException
      if (status === HttpStatus.BAD_REQUEST && typeof errorResponse === 'object') {
        const constraints = Object.values((errorResponse as any).message || {});
        message = `参数校验失败: ${constraints.join('; ')}`;
        code = BUSINESS_ERROR_CODE.COMMON; // 可选:转为业务错误码
      } else {
        message = typeof errorResponse === 'string'
          ? errorResponse
          : (errorResponse as any)?.message || '请求失败';
        code = status;
      }
    }
    // 3. 未预期的系统异常(500)
    else {
      this.logger.error('Unexpected error:', exception);
      status = HttpStatus.INTERNAL_SERVER_ERROR;
      code = status;
      message = '服务器内部错误,请稍后再试';
    }

    // 返回统一结构
    response.status(status).json({
      code,
      message,
      data: null,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

关键策略

场景处理方式
BusinessException返回 200 + 业务错误码(如 10002
DTO 校验失败可选择返回 400 或转为业务错误码(推荐后者,便于前端统一判断 code !== 0
未知异常记录日志 + 返回友好提示,绝不暴露堆栈

安全提示:生产环境严禁返回 exception.stack

3. 全局注册(main.ts)

ts
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe, VersioningType } from '@nestjs/common';
import { AppModule } from './app.module';
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
import { AllExceptionsFilter } from './common/exceptions/all-exceptions.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { cors: true });

  // ✅ DTO 校验(开启详细错误)
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
      // 可选:自定义校验错误格式(若 AllExceptionsFilter 已处理,此处可省略)
    }),
  );

  // ✅ 成功响应标准化
  app.useGlobalInterceptors(new TransformInterceptor());

  // ✅ 异常统一处理
  app.useGlobalFilters(new AllExceptionsFilter());

  // 其他配置...
  app.setGlobalPrefix('/api');
  await app.listen(3000);
}
bootstrap();

4. 业务错误码规范(避免魔法数字)

ts
// src/common/exceptions/business.error.code.ts
export const BUSINESS_ERROR_CODE = {
  COMMON: 10001,           // 通用错误
  TOKEN_INVALID: 10002,    // Token 无效
  ACCESS_FORBIDDEN: 10003, // 无权限
  USER_DISABLED: 10004,    // 账号禁用
} as const;

使用示例:

ts
throw new BusinessException({
  code: BUSINESS_ERROR_CODE.TOKEN_INVALID,
  message: '登录已过期,请重新登录',
});

5. 前端协作建议

  • 成功判断if (res.code === 0)
  • 错误处理else { showErrorMessage(res.message); }
  • 无需关心 HTTP 状态码(因业务异常也返回 200),简化逻辑

注:若团队坚持 RESTful 风格(校验失败返回 400),则 AllExceptionsFilter 中保留 status = 400 即可,但需前端同时检查 statuscode

6. 注意事项

  • 不要在拦截器中修改 null/undefined[] —— 违背数据契约
  • 文件下载、流式接口使用 @Res(),并确保不被 TransformInterceptor 包装
  • 生产环境关闭 ValidationPipedisableErrorMessages
  • 关键异常务必记录日志(如数据库连接失败、第三方 API 超时)

7. 总结

通过 TransformInterceptor + AllExceptionsFilter 的组合,我们实现了:

接口契约清晰
错误可追溯、可维护
前后端解耦,提升协作效率
防御式编程,防止敏感信息泄露

好的系统不是不出错,而是出错时依然优雅。

附:相关文件结构建议

src/
├── common/
│   ├── interceptors/
│   │   └── transform.interceptor.ts
│   └── exceptions/
│       ├── all-exceptions.filter.ts
│       ├── business.exception.ts
│       └── business.error.code.ts

本文档适用于 NestJS v9+ 项目,可根据团队实际需求调整错误码策略与响应结构。