Swagger / OpenAPI 接口文档
一、为什么需要 Swagger
Swagger(基于 OpenAPI 规范)自动从代码生成交互式 API 文档:
- 前后端联调时,前端直接在浏览器调用接口,无需 Postman
- 自动生成客户端 SDK(TypeScript、Python 等)
- 文档与代码同步,不会过时
二、安装与基础配置
bash
npm install @nestjs/swagger swagger-ui-expresstypescript
// 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 start→ http://localhost:3000/api
常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
| Swagger 文档为空 | Controller 未在 AppModule 注册 | 确认 Controller 所在 Module 已加入 imports |
泛型响应类型显示为 object | 泛型类未具体化 | 为每个泛型组合创建具体子类,如 UserListDto extends PaginatedDto<UserDto> |
@ApiBearerAuth 不显示锁图标 | DocumentBuilder 未调用 addBearerAuth | 在 DocumentBuilder 链中加 .addBearerAuth({...}, 'access-token') |
| Swagger 生产环境可访问 | 未限制访问 | NODE_ENV === 'production' 时不调用 SwaggerModule.setup() |