NestJS 🧑‍🍳 厨子必修课(九):API 文档 Swagger

1. 前言

后端 API 开发完成后,需要给前端一份接口文档,Swagger 能够帮助我们自动生成接口文档,这将会用到 @nestjs/swaggerswagger-ui-express

欢迎加入技术交流群

  1. NestJS 🧑‍🍳 厨子必修课(一):后端的本质
  2. NestJS 🧑‍🍳 厨子必修课(二):项目创建
  3. NestJS 🧑‍🍳 厨子必修课(三):控制器
  4. NestJS 🧑‍🍳 厨子必修课(四):服务类
  5. NestJS 🧑‍🍳 厨子必修课(五):Prisma 集成(上)
  6. NestJS 🧑‍🍳 厨子必修课(六):Prisma 集成(下)
  7. NestJS 🧑‍🍳 厨子必修课(七):管道
  8. 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

需要传 pageNumpageSize

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;
}

创建一个用户需要 nameemailpasswordrole,最后一个 role 不是必填项,不必加上 @ApiProperty 装饰器。

在请求 body 数据的 Schema 中可以看到 descriptionexample

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 就可以了,这是为什么呢?

原因

  1. Prisma 的数据模型文件已经定义了数据库结构:在 Prisma 中,schema.prisma 文件用于定义数据库模型,它描述了数据库表的结构和关系。相比于传统 ORM(如 TypeORM)的实体类,这个文件就相当于实体的定义。
  2. DTO 负责请求和响应的数据结构:DTO(Data Transfer Object)用于定义请求体和响应体的结构,并结合验证器(如 class-validator)进行数据验证和转换。它可以独立于数据库模型来使用,确保数据输入输出的安全和一致性。
  3. 减少重复定义:如果已经有 schema.prisma 文件和 DTO 类,再定义一个实体类会增加重复和维护的成本。使用 Prisma 时,主要依赖 PrismaClient 来执行数据库操作,直接使用 DTO 和 schema.prisma 文件即可满足大多数业务需求。

典型用法

  • Prisma 负责数据库的持久化:使用 schema.prisma 定义数据库表结构,通过 Prisma 的 PrismaClient 进行数据查询和操作。
  • DTO 负责请求和响应的数据格式:通过 DTO 类定义 API 请求体和响应体的结构,并结合 Swagger 生成 API 文档。

这种做法符合"分离关注点"的原则,使得数据库层(Prisma)和数据传输层(DTO)各司其职。

相关推荐
涡能增压发动积1 天前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
Wenweno0o1 天前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
于慨1 天前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz1 天前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
swg3213211 天前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
从前慢丶1 天前
前端交互规范(Web 端)
前端
tyung1 天前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald1 天前
SpringBoot - 自动配置原理
java·spring boot·后端
CHU7290351 天前
便捷约玩,沉浸推理:线上剧本杀APP功能版块设计详解
前端·小程序
GISer_Jing1 天前
Page-agent MCP结构
前端·人工智能