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 即可,但需前端同时检查 status 和 code。
6. 注意事项
- 不要在拦截器中修改
null/undefined为[]—— 违背数据契约 - 文件下载、流式接口使用
@Res(),并确保不被TransformInterceptor包装 - 生产环境关闭
ValidationPipe的disableErrorMessages - 关键异常务必记录日志(如数据库连接失败、第三方 API 超时)
7. 总结
通过 TransformInterceptor + AllExceptionsFilter 的组合,我们实现了:
接口契约清晰
错误可追溯、可维护
前后端解耦,提升协作效率
防御式编程,防止敏感信息泄露
好的系统不是不出错,而是出错时依然优雅。
附:相关文件结构建议
src/
├── common/
│ ├── interceptors/
│ │ └── transform.interceptor.ts
│ └── exceptions/
│ ├── all-exceptions.filter.ts
│ ├── business.exception.ts
│ └── business.error.code.ts本文档适用于 NestJS v9+ 项目,可根据团队实际需求调整错误码策略与响应结构。