1. 前言
后端 API 开发完成后,需要给前端一份接口文档,Swagger 能够帮助我们自动生成接口文档,这将会用到 @nestjs/swagger 和 swagger-ui-express。
欢迎加入技术交流群。

- NestJS 🧑🍳 厨子必修课(一):后端的本质
- NestJS 🧑🍳 厨子必修课(二):项目创建
- NestJS 🧑🍳 厨子必修课(三):控制器
- NestJS 🧑🍳 厨子必修课(四):服务类
- NestJS 🧑🍳 厨子必修课(五):Prisma 集成(上)
- NestJS 🧑🍳 厨子必修课(六):Prisma 集成(下)
- NestJS 🧑🍳 厨子必修课(七):管道
- NestJS 🧑🍳 厨子必修课(八):异常过滤器
2. 安装与初始化
2.1 安装
bash
npm install @nestjs/swagger swagger-ui-express
2.2 在 main.ts 中配置 Swagger
typescript
// ...
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// ...
// 配置 Swagger
const config = new DocumentBuilder()
.setTitle('NestJS API for Cook')
.setDescription('The NestJS API description for Cook')
.setVersion('0.1')
.build();
// 创建 API 文档
const document = SwaggerModule.createDocument(app, config);
// 启动 Swagger UI
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
接口文档的标题为:NestJS API for Cook,描述为:The NestJS API description for Cook,版本为 0.1,被设置在了 /api 路径下。
2.3 查看文档
进入 http://localhost:3000/api 就能在线查看接口文档:

可以看到之前写的接口都在上面。以 users 模块的接口为例,接口具体由以下内容构成:
- Parameters 👉 参数,包括 path 参数和 query 参数两类。
- Request body 👉 请求 body 数据,包括例子展示 example values 和数据类型 schema。(一般存在于 POST 和 PATCH 请求中)
- Responses 👉 响应,包括状态码 code 和描述 description。
POST /users

不需要传参数,但需要传 body 数据。
GET /users

需要传 pageNum 和 pageSize。
GET /users/search

需要传 query。
GET /users/{id}

这里需要传的 id 属于 path 参数,在代码中也是动态的。
PATCH /users/{id}

需要传 id 以及修改的 body 数据。
DELETE /users/{id}

删除只需要传递 id。
PATCH /users/{id}/role

修改指定用户的角色,要传递 id 以及 body 数据。
2.4 接口测试
可以看到每一个接口的右侧都有一个 Try it out 按钮,点击可以对接口进行测试。
3. 基础注解
现在的文档还存在 2 个问题,第一是没有区分 API 版本;第二是所有的接口都在 default 分组下没有按照模块来划分。
首先给路由全局设置 api 版本前缀:
typescript
// main.ts
app.setGlobalPrefix('api/v1');
再次打开 /api 查看,就设置上了前缀:

⚠️ 注意:和设置管道、过滤器那些类似,当然也为单个控制器设置。
@nestjs/swagger 包提供了一些装饰器用于对文档进行注解,下面以 users 为例。
3.1 @ApiTags()
@ApiTags() 用于对控制器进行分组。
diff
// users.controller.ts
@Controller('users')
+ @ApiTags('用户管理')
export class UsersController {
// ...
}

有关 users 的路由被分在"用户管理"类别下。
3.2 @ApiOperation()
@ApiOperation() 用于描述 API 的操作。
diff
// users.controller.ts
@Post()
+ @ApiOperation({ summary: '创建新用户' })
create(
@Body('name', UniqueUsernamePipe) name: string,
@Body() createUserDto: CreateUserDto,
) {
return this.usersService.create(createUserDto);
}
// ...

4. 高级注解
另外,无论是请求体还是响应体的文档内容都缺少描述,示例也过于简单。DTO 可以帮助定义请求体或响应体的数据结构,并通过 @ApiProperty() 注解为每个字段添加描述,自动生成文档。
4.1 @ApiProperty() - DTO(Data Transfer Object)传输层
DTO 用于定义数据传输的结构,通常在控制器中用于请求和响应的数据验证 ,会结合验证库 class-validator 来执行字段验证。
@ApiProperty() 除了为 DTO 中的字段添加描述 description ,还可以添加例子 example,这样就使得每个 API 的输入输出更加清晰。
users 模块的 DTO 有三个:
- create-user.dto.ts 创建用户
- update-user.dto.ts 更新用户
- update-user-role.dto.ts 更新用户角色
例如,创建用户的请求可以有一个 CreateUserDto 类,包含字段 name、email 等:
create-user.dto.ts
diff
import { UserRole } from '@prisma/client';
+ import { ApiProperty } from '@nestjs/swagger';
import {
IsNotEmpty,
MinLength,
IsEmail,
IsString,
IsEnum,
IsOptional,
} from 'class-validator';
export class CreateUserDto {
+ @ApiProperty({ description: '用户名', example: 'zhangsan' })
@IsString()
@IsNotEmpty()
@MinLength(3)
name: string;
+ @ApiProperty({ description: '用户邮箱', example: 'zhangsan@example.com' })
@IsEmail()
@IsNotEmpty()
email: string;
+ @ApiProperty({ description: '用户密码', example: 'password123' })
@IsString()
@IsNotEmpty()
@MinLength(8)
password: string;
@IsEnum(UserRole, { message: 'Role must be a valid user role' })
@IsOptional()
role: UserRole;
}
创建一个用户需要 name、email、password、role,最后一个 role 不是必填项,不必加上 @ApiProperty 装饰器。
在请求 body 数据的 Schema 中可以看到 description 和 example:

example 同时也作为 Example Value 中的示例值:

update-user.dto.ts
typescript
import { ApiProperty, PartialType } from '@nestjs/swagger';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {
@ApiProperty({ description: '用户名', example: 'lisi', required: false })
name?: string;
@ApiProperty({
description: '用户邮箱',
example: 'lisi@example.com',
required: false,
})
email?: string;
@ApiProperty({
description: '用户密码',
example: 'newpassword123',
required: false,
})
password?: string;
}

update-user-role.dto.ts
typescript
import { ApiProperty } from '@nestjs/swagger';
import { UserRole } from '../enums/user-role.enum';
export class UpdateUserRoleDto {
@ApiProperty({
description: '用户角色',
example: UserRole.ADMIN,
enum: UserRole,
})
role: UserRole;
}

enum 表示这个参数是一个枚举类型,具体值来自 UserRole:
ini
// ../enums/user-role.enum.ts
export enum UserRole {
USER = 'USER',
ADMIN = 'ADMIN',
}
这与 schema.prisma 中定义的保持一致:
schema
// ...
model User {
id Int @id @default(autoincrement())
email String @unique
name String
password String
role UserRole @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
}
enum UserRole {
USER
ADMIN
}
4.2 @ApiQuery() - 查询参数
对于创建和更新数据实体可以使用 @ApiProperty() 注解,而对于请求体的查询参数就要使用 @ApiQuery() 了。
diff
@Get()
@ApiOperation({ summary: '获取所有用户' })
+ @ApiQuery({ name: 'pageNum', description: '页码', required: false })
+ @ApiQuery({ name: 'pageSize', description: '每页条数', required: false })
findAll(
@Query('pageNum') pageNum?: number,
@Query('pageSize') pageSize?: number,
) {
return this.usersService.findAll(pageNum, pageSize);
}

上面的注解为文档的查询参数添加了描述 description 以及是否为必填项 required。
再比如搜索用户:
diff
@Get('search')
@ApiOperation({ summary: '搜索用户' })
+ @ApiQuery({ name: 'query', description: '搜索关键词', example: '张三' })
search(@Query('query') query: string) {
return this.usersService.searchUsers(query);
}

额外加上 example,示例更完善。
4.3 @ApiParam() - 路径参数
请求 URL 上路径参数则使用 @ApiParam() 来定义,也就是动态参数。
diff
@Get(':id')
@ApiOperation({ summary: '根据ID获取用户' })
+ @ApiParam({ name: 'id', description: '用户ID', example: 1 })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
// ...
上面以 GET 方法为例,PATCH、DELETE 同理。

4.4 @ApiBody() - 请求体结构
@ApiBody() 用于描述复杂的请求体结构,它和 DTO 一起用:
diff
@Post()
@ApiOperation({ summary: '创建新用户' })
+ @ApiBody({ type: CreateUserDto })
@ApiResponse({ status: 201, description: '用户创建成功' })
@ApiResponse({ status: 400, description: '无效的输入数据' })
create(
@Body('name', UniqueUsernamePipe) name: string,
@Body() createUserDto: CreateUserDto,
) {
return this.usersService.create(createUserDto);
}
4.5 @ApiResponse() - 状态码
@ApiResponse() 用于定义返回的响应状态码 status 和描述 description。
diff
@Post()
@ApiOperation({ summary: '创建新用户' })
@ApiBody({ type: CreateUserDto })
+ @ApiResponse({ status: 201, description: '用户创建成功' })
+ @ApiResponse({ status: 400, description: '无效的输入数据' })
create(
@Body('name', UniqueUsernamePipe) name: string,
@Body() createUserDto: CreateUserDto,
) {
return this.usersService.create(createUserDto);
}

5. 总结
本文介绍了 Swagger 在 NestJS 项目中的集成、配置以及注解用法,适合开发者在实际项目中生成清晰易读的 API 文档。其实除了 dto 文件,还有一个 entity 文件,它用于结合传统 ORM(如 TypeORM)定义数据实体,在笔者的教程中,使用 Prisma 作为 ORM,只有用 dto 就可以了,这是为什么呢?
原因
- Prisma 的数据模型文件已经定义了数据库结构:在 Prisma 中,schema.prisma 文件用于定义数据库模型,它描述了数据库表的结构和关系。相比于传统 ORM(如 TypeORM)的实体类,这个文件就相当于实体的定义。
- DTO 负责请求和响应的数据结构:DTO(Data Transfer Object)用于定义请求体和响应体的结构,并结合验证器(如 class-validator)进行数据验证和转换。它可以独立于数据库模型来使用,确保数据输入输出的安全和一致性。
- 减少重复定义:如果已经有 schema.prisma 文件和 DTO 类,再定义一个实体类会增加重复和维护的成本。使用 Prisma 时,主要依赖 PrismaClient 来执行数据库操作,直接使用 DTO 和 schema.prisma 文件即可满足大多数业务需求。
典型用法
- Prisma 负责数据库的持久化:使用 schema.prisma 定义数据库表结构,通过 Prisma 的 PrismaClient 进行数据查询和操作。
- DTO 负责请求和响应的数据格式:通过 DTO 类定义 API 请求体和响应体的结构,并结合 Swagger 生成 API 文档。
这种做法符合"分离关注点"的原则,使得数据库层(Prisma)和数据传输层(DTO)各司其职。