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:分页数据格式错误
解决方案 :确保返回数据包含 list
和 meta
字段
问题3:文件下载被拦截
解决方案:在拦截器中添加流响应判断逻辑
性能监控建议
ts
// 添加响应时间监控
const startTime = Date.now();
return next.handle().pipe(
tap(() => {
const duration = Date.now() - startTime;
this.logger.debug(`Response time: ${duration}ms`);
})
);