NestJS 统一响应格式最佳实践:标准化 API 返回结构

NestJS 统一响应格式最佳实践:标准化 API 返回结构

核心设计理念

响应格式标准化

ts 复制代码
// 成功响应结构
{
  "code": 200,
  "message": "Success",
  "data": T | PaginatedData<T>
}

// 分页数据结构
{
  "list": T[],
  "pageInfo": {
    "page": number,
    "pageSize": number,
    "total": number,
    "totalPages": number
  }
}

// 错误响应结构
{
  "code": number,
  "message": string,
  "data": null
}

快速开始指南

1. 安装依赖(如需要)

npm install class-transformer class-validator

2. 基础配置步骤

ts 复制代码
// main.ts - 最小化配置
app.useGlobalInterceptors(new ResponseInterceptor());
app.useGlobalFilters(new HttpExceptionFilter());

完整实现方案

响应数据模型定义

ts 复制代码
// common/dto/api-response.dto.ts
export class ApiResponse<T> {
  constructor(
    public code: number,
    public message: string,
    public data: T | PaginatedData<T> = null,
    public timestamp: string = new Date().toISOString()
  ) {}
}

export class PaginatedData<T> {
  constructor(
    public list: T[],
    public pageInfo: {
      page: number;
      pageSize: number;
      total: number;
      totalPages: number;
    }
  ) {}
}

智能响应拦截器

ts 复制代码
// common/interceptors/response.interceptor.ts
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    
    return next.handle().pipe(
      map((data) => this.formatResponse(data, request)),
      catchError(error => throwError(() => error))
    );
  }

  private formatResponse(data: any, request: Request) {
    // 跳过流响应(如文件下载)
    if (data instanceof StreamableFile) {
      return data;
    }

    // 处理分页数据(支持多种分页格式)
    if (this.isPaginatedData(data)) {
      const paginated = this.createPaginatedResponse(data);
      return new ApiResponse(200, 'Success', paginated);
    }

    // 处理空响应
    if (data === undefined || data === null) {
      return new ApiResponse(200, 'Success');
    }

    return new ApiResponse(200, 'Success', data);
  }

  private isPaginatedData(data: any): boolean {
    return (
      (data?.list && data?.meta) ||
      (data?.data && data?.pagination) ||
      (data?.items && data?.total)
    );
  }
}

增强型异常过滤器

ts 复制代码
// common/filters/http-exception.filter.ts
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger('ExceptionFilter');

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

    const { status, message } = this.parseException(exception);
    
    // 记录错误日志(500错误详细记录)
    if (status >= 500) {
      this.logger.error(
        `HTTP ${status} - ${message}`,
        exception instanceof Error ? exception.stack : '',
        'ExceptionFilter'
      );
    }

    response.status(status).json(
      new ApiResponse(
        status,
        message,
        null,
        new Date().toISOString()
      )
    );
  }

  private parseException(exception: unknown): { status: number; message: string } {
    if (exception instanceof HttpException) {
      const response = exception.getResponse();
      return {
        status: exception.getStatus(),
        message: this.getExceptionMessage(response)
      };
    }

    return {
      status: HttpStatus.INTERNAL_SERVER_ERROR,
      message: 'Internal server error'
    };
  }
}

高级配置选项

1. 自定义状态码映射

ts 复制代码
// common/constants/response-codes.constants.ts
export const RESPONSE_CODES = {
  SUCCESS: 200,
  CREATED: 201,
  BAD_REQUEST: 400,
  UNAUTHORIZED: 401,
  FORBIDDEN: 403,
  NOT_FOUND: 404,
  SERVER_ERROR: 500,
  // 业务状态码
  BUSINESS_ERROR: 1000,
  VALIDATION_ERROR: 1001
};

2. 环境特定配置

ts 复制代码
// config/response.config.ts
export const responseConfig = () => ({
  response: {
    enableLogging: process.env.NODE_ENV !== 'production',
    showStackTrace: process.env.NODE_ENV === 'development',
    // 可跳过的路径(如健康检查)
    excludePaths: ['/health', '/metrics']
  }
});

3. 性能优化配置

ts 复制代码
// 跳过大文件响应的拦截
if (request.headers['content-length'] > 1024 * 1024) { // 1MB
  return data;
}

使用示例

控制器实现

ts 复制代码
@Controller('api/v1/users')
export class UserController {
  
  // 普通查询
  @Get(':id')
  async getUser(@Param('id') id: string) {
    return this.userService.findById(id); // 自动包装
  }

  // 分页查询
  @Get()
  async getUsers(@Query() dto: PaginationDto) {
    const result = await this.userService.paginate(dto);
    return {
      list: result.data,
      meta: {
        page: dto.page,
        pageSize: dto.pageSize,
        total: result.total
      }
    };
  }

  // 创建操作
  @Post()
  @HttpCode(201)
  async createUser(@Body() dto: CreateUserDto) {
    const user = await this.userService.create(dto);
    return user; // 自动转换为201状态码
  }
}

服务层分页实现

ts 复制代码
async paginate(dto: PaginationDto) {
  const [data, total] = await this.repository.findAndCount({
    where: { isActive: true },
    skip: (dto.page - 1) * dto.pageSize,
    take: dto.pageSize,
    order: { createdAt: 'DESC' }
  });

  return { 
    data: plainToInstance(UserResponseDto, data),
    total 
  };
}

响应示例

成功响应

json 复制代码
{
  "code": 200,
  "message": "Success",
  "data": {
    "id": 1,
    "name": "张三",
    "email": "zhangsan@example.com"
  },
  "timestamp": "2024-01-01T10:30:00.000Z"
}

分页响应

json 复制代码
{
  "code": 200,
  "message": "Success",
  "data": {
    "list": [
      {"id": 1, "name": "用户1"},
      {"id": 2, "name": "用户2"}
    ],
    "pageInfo": {
      "page": 1,
      "pageSize": 10,
      "total": 150,
      "totalPages": 15
    }
  }
}

错误响应

json 复制代码
{
  "code": 404,
  "message": "用户不存在",
  "data": null,
  "timestamp": "2024-01-01T10:30:00.000Z"
}

常见问题排查

问题1:拦截器不生效

​解决方案​​:检查模块导入顺序和全局拦截器注册

问题2:分页数据格式错误

​解决方案​ ​:确保返回数据包含 listmeta字段

问题3:文件下载被拦截

​解决方案​​:在拦截器中添加流响应判断逻辑

性能监控建议

ts 复制代码
// 添加响应时间监控
const startTime = Date.now();
return next.handle().pipe(
  tap(() => {
    const duration = Date.now() - startTime;
    this.logger.debug(`Response time: ${duration}ms`);
  })
);
相关推荐
karry_k2 小时前
CopyOnWriteArraySet
后端
SamsongSSS2 小时前
Django之APPEND_SLASH配置爬坑
后端·python·django
EMQX2 小时前
ESP32 + MCP over MQTT:基于大模型打造人格化情感智能体
后端
karry_k2 小时前
为什么CopyOnWriteArrayList是线程安全的?
后端
hello 早上好3 小时前
Spring Boot 核心启动机制与配置原理剖析
java·spring boot·后端
武子康3 小时前
大数据-112 Flink DataStream API :数据源、转换与输出 文件、Socket 到 Kafka 的完整流程
大数据·后端·flink
Terio_my5 小时前
Spring Boot 缓存技术
spring boot·后端·缓存
IT_陈寒5 小时前
Python 3.12 性能暴增50%!这5个新特性让老项目直接起飞
前端·人工智能·后端
你的人类朋友5 小时前
【操作系统】说说 x86 和 x64
后端·程序员·操作系统