You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在NestJS Swagger中配置统一自定义响应格式(无需为每个API创建单独响应类)

如何在NestJS中复用Swagger自定义响应格式,避免重复创建响应类

这个问题我之前也遇到过——为每个接口写单独的响应类确实太繁琐了,尤其是当所有接口都遵循统一的响应格式时。下面是我在NestJS+Swagger项目中复用响应结构的解决方案,既能保持格式统一,又能灵活定制不同接口的data部分:

1. 创建泛型基础响应类

首先定义一个泛型的基础响应类,作为所有接口响应的模板。泛型的作用是让我们可以灵活传入data部分的类型:

import { ApiProperty } from "@nestjs/swagger";

// 泛型类,T代表data字段的类型,默认是any
export class SuccessResponse<T = any> {
  @ApiProperty({ example: 200, description: 'HTTP状态码' })
  statusCode: number;

  @ApiProperty({ example: 'success', description: '响应状态标识' })
  status: string;

  @ApiProperty({ description: '响应提示信息', required: false })
  message?: string;

  @ApiProperty({ description: '响应数据', required: false })
  data?: T;
}

2. 定义接口专属的Data子模型

对于需要返回特定数据结构的接口(比如登录接口的令牌+用户信息),单独定义对应的子模型,不用重复写整个响应结构:

import { ApiProperty } from "@nestjs/swagger";

// 用户信息子模型
class UserInfo {
  @ApiProperty({ example: '张三', description: '用户名' })
  name: string;

  @ApiProperty({ example: 'zhangsan@example.com', description: '邮箱' })
  email: string;

  @ApiProperty({ example: '60d21b4667d0d8992e610c85', description: '用户ID' })
  id: string;
}

// 登录返回的令牌数据模型
class AuthTokenData {
  @ApiProperty({ example: 3600, description: '令牌过期时间(秒)' })
  expiresIn: number;

  @ApiProperty({ example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', description: '访问令牌' })
  accessToken: string;

  @ApiProperty({ type: UserInfo })
  user: UserInfo;
}

3. 在Swagger中复用基础响应类

NestJS的Swagger支持用ApiExtraModelsgetSchemaPath来合并泛型模板和自定义子模型,避免重复代码。

首先在控制器上声明要用到的子模型,然后在接口的ApiOkResponse/ApiCreatedResponse中用allOf合并基础响应模板和自定义结构:

import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { ApiOkResponse, ApiCreatedResponse, ApiExtraModels, getSchemaPath } from '@nestjs/swagger';
import { SuccessResponse } from './success.response';
import { AuthService } from './auth.service';
import { AuthRegisterLoginDto, AuthLoginDto } from './dto';

// 告诉Swagger要处理这些子模型
@ApiExtraModels(AuthTokenData, UserInfo)
@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  // 登录接口示例:返回令牌+用户信息
  @Post('login')
  @HttpCode(HttpStatus.OK)
  @ApiOkResponse({
    description: '登录成功',
    schema: {
      allOf: [
        // 引用基础响应模板
        { $ref: getSchemaPath(SuccessResponse) },
        // 自定义当前接口的响应细节:statusCode示例、data结构
        {
          properties: {
            statusCode: { example: 200 },
            status: { example: 'success' },
            message: { example: '登录成功' },
            data: { $ref: getSchemaPath(AuthTokenData) }
          }
        }
      ]
    }
  })
  async login(@Body() authLoginDto: AuthLoginDto) {
    return this.authService.login(authLoginDto);
  }

  // 注册接口示例:可以选择不返回data,只返回状态信息
  @Post('email/register')
  @HttpCode(HttpStatus.CREATED)
  @ApiCreatedResponse({
    description: '用户创建成功',
    schema: {
      allOf: [
        { $ref: getSchemaPath(SuccessResponse) },
        {
          properties: {
            statusCode: { example: 201 },
            status: { example: 'success' },
            message: { example: '用户注册成功' }
            // 如果需要返回用户信息,取消下面的注释即可
            // data: { $ref: getSchemaPath(UserInfo) }
          }
        }
      ]
    }
  })
  async register(@Body() authRegisterLoginDto: AuthRegisterLoginDto) {
    return this.authService.register(authRegisterLoginDto);
  }
}

4. 封装工具函数进一步简化代码

如果觉得每个接口都写allOf太麻烦,可以封装一个工具函数来统一生成响应配置:

import { ApiResponseOptions, getSchemaPath } from '@nestjs/swagger';
import { SuccessResponse } from './success.response';

export function buildSuccessResponse<T>(options: {
  description: string;
  statusCode: number;
  dataSchema?: { $ref: string };
  example?: Partial<SuccessResponse<T>>;
}): ApiResponseOptions {
  const baseSchema = { $ref: getSchemaPath(SuccessResponse) };
  const customProperties: any = {
    statusCode: { example: options.statusCode },
    status: { example: 'success' }
  };

  // 添加自定义data结构
  if (options.dataSchema) {
    customProperties.data = options.dataSchema;
  }

  // 合并自定义示例
  if (options.example) {
    Object.entries(options.example).forEach(([key, value]) => {
      customProperties[key] = { example: value };
    });
  }

  return {
    description: options.description,
    schema: { allOf: [baseSchema, { properties: customProperties }] }
  };
}

之后在接口中使用这个工具函数,代码会简洁很多:

@Post('login')
@HttpCode(HttpStatus.OK)
@ApiOkResponse(buildSuccessResponse({
  description: '登录成功',
  statusCode: 200,
  dataSchema: { $ref: getSchemaPath(AuthTokenData) },
  example: { message: '登录成功' }
}))
async login(@Body() authLoginDto: AuthLoginDto) {
  return this.authService.login(authLoginDto);
}

5. 确保实际返回结构与Swagger定义一致

最后别忘了在你的服务层方法中返回符合SuccessResponse结构的数据,比如:

async login(dto: AuthLoginDto): Promise<SuccessResponse<AuthTokenData>> {
  // 执行登录逻辑...获取token和用户信息
  const tokenData = {
    expiresIn: 3600,
    accessToken: '生成的JWT令牌',
    user: { name: dto.email.split('@')[0], email: dto.email, id: '用户MongoDB ID' }
  };

  return {
    statusCode: HttpStatus.OK,
    status: 'success',
    message: '登录成功',
    data: tokenData
  };
}

这样配置后,Swagger就能正确展示每个接口的自定义响应格式,同时你也不用为每个接口单独创建响应类了。

内容的提问来源于stack exchange,提问作者monkeyUser

火山引擎 最新活动