Skip to content

统一响应结构

字段约定

NestJS 和 HTTP 没有强制要求统一响应结构。codemessagedata 是项目约定,不是框架标准;

采用统一结构后,普通 JSON API 的成功和错误响应都必须遵循同一个 ApiResponse<T> 结构:

ts
export interface ApiResponse<T = unknown> {
  code: number
  message: string
  data?: T
}
字段类型说明
codenumber稳定的业务状态码,0 表示成功,非零值表示失败。
messagestring面向用户或开发者的结果说明,不作为客户端分支判断条件。
dataT可选;成功响应存在数据时返回。

目录结构

bash
src/
├── common/
   ├── interfaces/
   └── api-response.interface.ts    # code、message、data 响应结构
   └── constants/
       └── common-response.constant.ts  # 通用成功和错误 code、message
└── modules/
    └── users/
        └── constants/
            └── user-response.constant.ts # 用户业务 code 和 message

业务码分配

范围归属
0成功
10000–19999通用错误
20000–99999业务错误

20000–99999 内的模块划分由具体项目决定。HTTP 状态码表达协议层结果,业务码区分具体业务结果;业务码发布后不修改含义,也不重新分配。

响应定义管理

code 和默认 message 不单独维护,而是组成完整响应定义。通用响应定义放在 common-response.constant.ts,业务响应定义放在对应模块的 *-response.constant.ts

ts
export const CommonResponse = {
  SUCCESS: { code: 0, message: '请求成功' },
  CREATED: { code: 0, message: '创建成功' },
  UPDATED: { code: 0, message: '更新成功' },
  DELETED: { code: 0, message: '删除成功' },
  VALIDATION_FAILED: { code: 10001, message: '请求参数校验失败' },
  REQUEST_FAILED: { code: 10002, message: '请求失败' },
  INTERNAL_SERVER_ERROR: { code: 10003, message: '服务器内部错误' },
} as const
ts
export const UserResponse = {
  USER_NOT_FOUND: { code: 20001, message: '用户不存在' },
  EMAIL_ALREADY_EXISTS: { code: 20002, message: '邮箱已被注册' },
} as const

客户端根据 code 判断结果,不解析 message。参数校验失败时使用第一条具体消息,默认消息仅作为兜底;未知异常只返回通用消息,不暴露堆栈、SQL 或第三方原始错误。

响应示例

请求成功时返回对应的 2xx HTTP 状态码:

json
{
  "code": 0,
  "message": "请求成功",
  "data": {
    "id": 1,
    "name": "Vfan"
  }
}

成功但没有响应数据时省略 data

json
{
  "code": 0,
  "message": "请求成功"
}

业务资源不存在时返回 404 Not Found

json
{
  "code": 20001,
  "message": "用户不存在"
}

输入校验失败时返回 400 Bad Requestmessage 使用第一条具体错误:

json
{
  "code": 10001,
  "message": "email must be an email"
}

客户端根据 HTTP 状态码识别错误类别,根据 code 区分具体业务结果;不要解析可能调整或国际化的 message。错误定义应集中管理,避免在代码中散落业务码和消息字符串。

统一成功响应

使用全局 Interceptor 包装 Controller 返回的数据:

ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
import type { Observable } from 'rxjs'
import { map } from 'rxjs'
import { CommonResponse } from '../constants/common-response.constant'
import type { ApiResponse } from '../interfaces/api-response.interface'

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
  intercept(_context: ExecutionContext, next: CallHandler<T>): Observable<ApiResponse<T>> {
    return next.handle().pipe(
      map((data) => ({
        ...CommonResponse.SUCCESS,
        ...(data === undefined ? {} : { data }),
      })),
    )
  }
}

Interceptor 只转换成功结果,不要捕获异常后返回伪成功响应。

统一错误响应

ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common'
import type { Response } from 'express'
import { CommonResponse } from '../constants/common-response.constant'
import type { ApiResponse } from '../interfaces/api-response.interface'

@Catch()
export class HttpErrorFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const response = host.switchToHttp().getResponse<Response>()
    const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR
    const exceptionResponse = exception instanceof HttpException ? exception.getResponse() : null
    const body: Record<string, unknown> =
      typeof exceptionResponse === 'object' && exceptionResponse !== null
        ? (exceptionResponse as Record<string, unknown>)
        : {}
    const rawMessage = typeof exceptionResponse === 'string' ? exceptionResponse : body.message
    const isValidationError = Array.isArray(rawMessage)
    const firstValidationMessage = isValidationError
      ? rawMessage.find((message): message is string => typeof message === 'string')
      : undefined
    const isDefinedError = status < 500 && typeof body.code === 'number' && typeof body.message === 'string'
    const fallbackError = isValidationError
      ? CommonResponse.VALIDATION_FAILED
      : status >= 500
        ? CommonResponse.INTERNAL_SERVER_ERROR
        : CommonResponse.REQUEST_FAILED

    const result: ApiResponse<unknown> = {
      code: isDefinedError ? (body.code as number) : fallbackError.code,
      message: isValidationError
        ? (firstValidationMessage ?? fallbackError.message)
        : isDefinedError
          ? (body.message as string)
          : fallbackError.message,
    }

    response.status(status).json(result)
  }
}

业务主动抛出异常

ts
import { NotFoundException } from '@nestjs/common'
import { UserResponse } from './constants/user-response.constant'

throw new NotFoundException(UserResponse.USER_NOT_FOUND)

全局注册

使用 APP_INTERCEPTORAPP_FILTER 注册后,实例由依赖注入容器管理:

ts
import { Module } from '@nestjs/common'
import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'
import { HttpErrorFilter } from './common/filters/http-error.filter'
import { TransformInterceptor } from './common/interceptors/transform.interceptor'

@Module({
  providers: [
    { provide: APP_INTERCEPTOR, useClass: TransformInterceptor },
    { provide: APP_FILTER, useClass: HttpErrorFilter },
  ],
})
export class AppModule {}

参考

基于 MIT 许可发布