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)各司其职。

相关推荐
a程序小傲14 小时前
小红书Java面试被问:TCC事务的悬挂、空回滚问题解决方案
java·开发语言·人工智能·后端·python·面试·职场和发展
北辰alk14 小时前
2025:当Vibe Coding成为我的创意画布——一名前端工程师的AI元年记
前端·trae
短剑重铸之日14 小时前
《SpringBoot4.0初识》第五篇:实战代码
java·后端·spring·springboot4.0
jump_jump14 小时前
SaaS 时代已死,SaaS 时代已来
前端·后端·架构
a努力。14 小时前
国家电网Java面试被问:最小生成树的Kruskal和Prim算法
java·后端·算法·postgresql·面试·linq
Yanni4Night14 小时前
Parcel 作者:如何用静态Hermes把JavaScript编译成C语言
前端·javascript·rust
hellokatewj14 小时前
前端 Promise 全解:从原理到面试
前端
superman超哥14 小时前
Rust Vec的内存布局与扩容策略:动态数组的高效实现
开发语言·后端·rust·动态数组·内存布局·rust vec·扩容策略
天意pt15 小时前
Blog-SSR 系统操作手册(v1.0.0)
前端·vue.js·redis·mysql·docker·node.js·express