Skip to content

响应数据统一格式化

全局拦截:后置拦截,拦截数据并统一数据格式

跳过格式化:比如文件上传等接口需要跳过格式化,借助前置拦截器和元信息实现

自定义提示语:以后置拦截器和元信息的方式实现自定义提示语 :::

1、定义拦截器

ts
// src/common/interceptors/transform.interceptor.ts
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiResponse } from '../types/response.type';
import { SKIP_TRANSFORM } from '../decorators/skip-transform.decorator';
import { RESPONSE_MESSAGE } from '../decorators/response-message.decorator';

@Injectable()
export class TransformInterceptor<T>
  implements NestInterceptor<T, ApiResponse<T>>
{
  constructor(private reflector: Reflector) {} // 注入Reflector
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<ApiResponse<T>> {
    // 前置拦截判断是否标记了跳过包装
    const skip = this.reflector.get<boolean>(
      SKIP_TRANSFORM,
      context.getHandler(),
    );
    if (skip) {
      return next.handle(); // 直接返回原始数据
    }

    const message =
      this.reflector.get<string>(RESPONSE_MESSAGE, context.getHandler()) ||
      'success';
    // 后置拦截包装:统一响应数据格式
    return next.handle().pipe(
      map((data) => ({
        code: 200, // 正常响应默认业务码200
        message,
        path: context.switchToHttp().getRequest().path,
        method: context.switchToHttp().getRequest().method,
        data: data ?? null, // 处理data为undefined的情况
        timestamp: Date.now(),
      })),
    );
  }
}

2、定义格式化类型

ts
// src/common/types/response.type.ts
/**
 * 统一响应格式
 */
export interface ApiResponse<T = any> {
  code: number;
  message: string;
  data: T;
  path: string;
  method: string;
  timestamp: number;
  error?: string;
  stack?: string;
}

3、跳过格式化

定义元信息

ts
// src/common/decorators/skip-transform.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const SKIP_TRANSFORM = 'SKIP_TRANSFORM';
export const SkipTransform = () => SetMetadata(SKIP_TRANSFORM, true);

4、自定义提示语

定义元信息

ts
// src/common/decorators/response-message.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const RESPONSE_MESSAGE = 'RESPONSE_MESSAGE';
export const ResponseMessage = (message: string) => SetMetadata(RESPONSE_MESSAGE, message);

5、注册全局拦截器

ts
import { TransformInterceptor } from './common/interceptors/transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 注册全局拦截器
  app.useGlobalInterceptors(new TransformInterceptor(app.get(Reflector)));

  app.setGlobalPrefix('api/v1');
  await app.listen(3000);
}
bootstrap();

测试

  1. 自定义提示语&数据格式化测试
shell
# 接口请求
http://localhost:3000/api/v1/user/2

# 在 src/user/user.controller.ts 中,给路由注入提示语元信息
import { ResponseMessage } from '../common/decorators/response-message.decorator';
@Get(':id')
@ResponseMessage('获取用户成功') # 自定义提示语
async findOne(@Param('id') id: number) {
    return await this.userService.findOne(id);
}

# 结果
{
    "code": 200,
    "message": "获取用户成功",
    "path": "/api/v1/user/2",
    "method": "GET",
    "data": {
        "id": 2,
        "username": "lisi",
        "version": 1
    },
    "timestamp": 1765269321713
}
  1. 跳过格式化测试
shell
# 接口请求
http://localhost:3000/api/v1/user

# 在 src/user/user.controller.ts 中,给路由注入跳过格式化元信息
import { SkipTransform } from '../common/decorators/skip-transform.decorator';
@Get()
@SkipTransform() # 跳过响应数据格式化
async findAll() {
    return await this.userService.findAll();
}

# 结果
[
    {
        "id": 2,
        "username": "lisi",
        "version": 1
    },
    {
        "id": 3,
        "username": "alias",
        "version": 1
    }
]