Skip to content

Swagger / OpenAPI 接口文档

一、为什么需要 Swagger

Swagger(基于 OpenAPI 规范)自动从代码生成交互式 API 文档:

  • 前后端联调时,前端直接在浏览器调用接口,无需 Postman
  • 自动生成客户端 SDK(TypeScript、Python 等)
  • 文档与代码同步,不会过时

二、安装与基础配置

bash
npm install @nestjs/swagger swagger-ui-express
typescript
// main.ts
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

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

  // 构建文档配置
  const config = new DocumentBuilder()
    .setTitle('NestJS API')
    .setDescription('后端接口文档')
    .setVersion('1.0')
    .addBearerAuth(               // 添加 JWT 认证支持
      {
        type: 'http',
        scheme: 'bearer',
        bearerFormat: 'JWT',
        description: '输入 JWT Token',
      },
      'access-token',             // 认证方案名称(在 @ApiBearerAuth() 中引用)
    )
    .addTag('认证', '注册、登录、Token 刷新')
    .addTag('用户', '用户 CRUD')
    .addTag('文章', '文章 CRUD')
    .addServer('http://localhost:3000', '本地开发')
    .addServer('https://api.example.com', '生产环境')
    .build();

  const document = SwaggerModule.createDocument(app, config);

  // 挂载到 /api 路径
  SwaggerModule.setup('api', app, document, {
    swaggerOptions: {
      persistAuthorization: true,   // 刷新页面保留认证信息
      filter: true,                 // 显示搜索过滤框
      displayRequestDuration: true, // 显示请求耗时
    },
    customSiteTitle: 'NestJS API 文档',
  });

  await app.listen(3000);
  console.log('Swagger: http://localhost:3000/api');
}

三、Controller 注解

typescript
import {
  ApiTags, ApiOperation, ApiResponse, ApiBearerAuth,
  ApiParam, ApiQuery, ApiBody,
} from '@nestjs/swagger';

@ApiTags('文章')           // 分组标签
@ApiBearerAuth('access-token')  // 表示该 Controller 需要认证
@Controller('posts')
export class PostsController {
  constructor(private postsService: PostsService) {}

  @ApiOperation({
    summary: '获取文章列表',
    description: '支持分页、关键词搜索和状态过滤',
  })
  @ApiQuery({ name: 'page', required: false, type: Number, example: 1 })
  @ApiQuery({ name: 'limit', required: false, type: Number, example: 10 })
  @ApiQuery({ name: 'keyword', required: false, type: String })
  @ApiResponse({ status: 200, description: '成功', type: PostListResponseDto })
  @ApiResponse({ status: 401, description: '未认证' })
  @Get()
  findAll(@Query() query: PaginationDto) {
    return this.postsService.findAll(query);
  }

  @ApiOperation({ summary: '获取单篇文章' })
  @ApiParam({ name: 'id', type: Number, description: '文章 ID', example: 1 })
  @ApiResponse({ status: 200, type: PostResponseDto })
  @ApiResponse({ status: 404, description: '文章不存在' })
  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.postsService.findOne(id);
  }

  @ApiOperation({ summary: '创建文章' })
  @ApiBody({ type: CreatePostDto })
  @ApiResponse({ status: 201, type: PostResponseDto })
  @Post()
  create(@Body() dto: CreatePostDto, @CurrentUser() user: User) {
    return this.postsService.create(dto, user.id);
  }
}

四、DTO 注解

基础属性描述

typescript
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class CreatePostDto {
  @ApiProperty({
    description: '文章标题',
    example: 'NestJS 入门指南',
    minLength: 1,
    maxLength: 200,
  })
  @IsString()
  @MinLength(1)
  @MaxLength(200)
  title: string;

  @ApiProperty({
    description: '文章内容(支持 Markdown)',
    example: '# 标题\n\n正文内容...',
  })
  @IsString()
  content: string;

  @ApiPropertyOptional({   // 可选字段
    description: '文章标签 ID 列表',
    type: [Number],
    example: [1, 2, 3],
  })
  @IsOptional()
  @IsArray()
  @IsInt({ each: true })
  tagIds?: number[];

  @ApiProperty({
    description: '是否发布',
    default: false,
  })
  @IsBoolean()
  @IsOptional()
  published?: boolean = false;
}

枚举类型

typescript
export enum PostStatus {
  DRAFT = 'draft',
  PUBLISHED = 'published',
  ARCHIVED = 'archived',
}

export class UpdatePostDto {
  @ApiPropertyOptional({
    enum: PostStatus,
    enumName: 'PostStatus',   // 在 Swagger 文档中生成枚举定义
    example: PostStatus.PUBLISHED,
  })
  @IsOptional()
  @IsEnum(PostStatus)
  status?: PostStatus;
}

响应 DTO(控制输出字段)

typescript
export class PostResponseDto {
  @ApiProperty({ example: 1 })
  id: number;

  @ApiProperty({ example: 'NestJS 入门指南' })
  title: string;

  @ApiProperty({ example: '2024-03-10T08:00:00.000Z' })
  createdAt: Date;

  @ApiProperty({ type: () => UserSummaryDto })  // 嵌套 DTO
  author: UserSummaryDto;

  // 不加 @ApiProperty 的字段不出现在文档中
  // (配合 ClassSerializerInterceptor 的 @Exclude() 使用)
}

export class UserSummaryDto {
  @ApiProperty({ example: 1 })
  id: number;

  @ApiProperty({ example: '张三' })
  name: string;
}

分页响应封装

typescript
// 泛型响应 DTO
export class PaginatedResponseDto<T> {
  @ApiProperty({ isArray: true })
  items: T[];

  @ApiProperty({ example: 100 })
  total: number;

  @ApiProperty({ example: 1 })
  page: number;

  @ApiProperty({ example: 10 })
  limit: number;

  @ApiProperty({ example: 10 })
  totalPages: number;
}

// 具体化泛型(Swagger 需要具体类型)
export class PostListResponseDto extends PaginatedResponseDto<PostResponseDto> {
  @ApiProperty({ type: [PostResponseDto] })
  items: PostResponseDto[];
}

五、PartialType、OmitType、PickType

@nestjs/swagger 提供的工具类型(同时生成 Swagger 文档):

typescript
import { PartialType, OmitType, PickType, IntersectionType } from '@nestjs/swagger';

// 更新 DTO:所有字段变为可选
export class UpdatePostDto extends PartialType(CreatePostDto) {}

// 省略敏感字段
export class PostPublicDto extends OmitType(CreatePostDto, ['authorId'] as const) {}

// 只选择部分字段
export class PostSummaryDto extends PickType(CreatePostDto, ['title', 'published'] as const) {}

// 合并两个 DTO
export class CreatePostWithMetaDto extends IntersectionType(
  CreatePostDto,
  PostMetaDto,
) {}

六、认证接口文档

typescript
@ApiTags('认证')
@Controller('auth')
export class AuthController {
  @ApiOperation({ summary: '用户登录' })
  @ApiBody({ type: LoginDto })
  @ApiResponse({
    status: 200,
    description: '登录成功,返回 JWT Token',
    schema: {
      example: {
        access_token: 'eyJhbGciOiJIUzI1NiJ9...',
        refresh_token: 'eyJhbGciOiJIUzI1NiJ9...',
        expires_in: 900,
      },
    },
  })
  @ApiResponse({ status: 401, description: '邮箱或密码错误' })
  @HttpCode(200)
  @UseGuards(AuthGuard('local'))
  @Post('login')
  login(@CurrentUser() user: User) {
    return this.authService.generateTokens(user);
  }

  // @ApiBearerAuth 表示该接口需要在 Swagger UI 中填入 Token
  @ApiBearerAuth('access-token')
  @ApiOperation({ summary: '获取当前用户信息' })
  @Get('me')
  getMe(@CurrentUser() user: User) {
    return user;
  }
}

七、生产环境隐藏文档

typescript
// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 只在非生产环境暴露 Swagger
  if (process.env.NODE_ENV !== 'production') {
    const config = new DocumentBuilder().setTitle('API').build();
    const document = SwaggerModule.createDocument(app, config);
    SwaggerModule.setup('api', app, document);
  }

  await app.listen(3000);
}

八、导出 OpenAPI JSON(供前端生成客户端)

typescript
// scripts/generate-api-spec.ts
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { writeFileSync } from 'fs';

async function generateSpec() {
  const app = await NestFactory.create(AppModule, { logger: false });
  const config = new DocumentBuilder().setTitle('API').setVersion('1.0').build();
  const document = SwaggerModule.createDocument(app, config);

  writeFileSync('./openapi.json', JSON.stringify(document, null, 2));
  await app.close();
  console.log('✓ openapi.json 生成完毕');
}

generateSpec();
bash
# 前端用 openapi-typescript 生成类型
npx openapi-typescript openapi.json -o src/types/api.d.ts

# 或用 openapi-generator 生成完整客户端
npx @openapitools/openapi-generator-cli generate \
  -i openapi.json -g typescript-fetch -o src/api-client

可运行 Demo: practice/07-extensions — 包含 Swagger 完整注解 Demo,npm starthttp://localhost:3000/api


常见错误

错误原因解决
Swagger 文档为空Controller 未在 AppModule 注册确认 Controller 所在 Module 已加入 imports
泛型响应类型显示为 object泛型类未具体化为每个泛型组合创建具体子类,如 UserListDto extends PaginatedDto<UserDto>
@ApiBearerAuth 不显示锁图标DocumentBuilder 未调用 addBearerAuthDocumentBuilder 链中加 .addBearerAuth({...}, 'access-token')
Swagger 生产环境可访问未限制访问NODE_ENV === 'production' 时不调用 SwaggerModule.setup()

NestJS 深度学习体系