NestJS 搭建博客系统(六)— 使用 Swagger 生成文档
之前的例子我们基本完整的实现了一个模块的curd,而这期间我们都在postman或者别的工具上反复输入地址,这实在是很难受,特别在团队开发期间,碎片化提供接口简直能让人抓狂,所以我们需要编写文档,最开始我是使用 yapi 的,但是后来觉得还是比较麻烦,需要手写文档,所以又找上了 swagger,但是 swagger 文档实在太丑,难以使用,最终就成了 swagger + yapi 或者 swagger + postman
开干
安装依赖
swagger 需要根据 nest 的底层库来选择,
express:
yarn add @nestjs/swagger swagger-ui-express
fastify:
yarn add @nestjs/swagger fastify-swagger
配置swagger
// src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filters/http-exception.filter';
import { TransformInterceptor } from './interceptor/transform.interceptor';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe())
app.useGlobalInterceptors(new TransformInterceptor())
app.useGlobalFilters(new HttpExceptionFilter())
const options = new DocumentBuilder()
.setTitle('blog-serve')
.setDescription('接口文档')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('swagger-doc', app, document);
await app.listen(3000);
}
bootstrap();
此时打开 localhost:3000/swagger-doc即可看到 swagger 文档
但是也只有接口,没有其他信息,到 DTO 增加入参信息
入参
在 DTO 文件即可修改文档中的入参描述
// src/modules/article/dto/id.dto.ts
import { IsNotEmpty, Matches } from "class-validator";
import { regPositive } from "src/utils/regex.util";
import { ApiProperty } from "@nestjs/swagger";
export class IdDTO {
@ApiProperty({
description: '文章id',
example: 1,
})
@Matches(regPositive, { message: '请输入有效 id' })
@IsNotEmpty({ message: 'id 不能为空' })
readonly id: number
}
// src/modules/article/dto/list.dto.ts
import { IsOptional, Matches } from "class-validator";
import { regPositiveOrEmpty } from "src/utils/regex.util";
import { ApiProperty } from "@nestjs/swagger";
export class ListDTO {
@ApiProperty({
description: '第几页',
example: 1,
required: false,
})
@IsOptional()
@Matches(regPositiveOrEmpty, { message: 'page 不可小于 0' })
readonly page?: number;
@ApiProperty({
description: '每页数据条数',
example: 10,
required: false,
})
@IsOptional()
@Matches(regPositiveOrEmpty, { message: 'pageSize 不可小于 0' })
readonly pageSize?: number;
}
// src/modules/article/dto/article-create.dto.ts
import { IsNotEmpty } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class ArticleCreateDTO {
@ApiProperty({
description: '文章标题',
example: '啊!美丽的大海',
})
@IsNotEmpty({ message: '请输入文章标题' })
readonly title: string;
@ApiProperty({
description: '文章描述/简介',
example: '给你讲述美丽的大海',
})
@IsNotEmpty({ message: '请输入文章描述' })
readonly description: string;
@ApiProperty({
description: '文章内容',
example: '啊!美丽的大海,你是如此美丽',
})
@IsNotEmpty({ message: '请输入文章内容' })
readonly content: string;
}
// src/modules/article/dto/article-edit.dto.ts
import { IsNotEmpty, IsOptional } from "class-validator";
import { IdDTO } from "./id.dto";
import { ApiProperty } from "@nestjs/swagger";
export class ArticleEditDTO extends IdDTO {
@ApiProperty({
description: '文章标题',
example: '啊!美丽的大海',
required: false,
})
@IsOptional()
@IsNotEmpty({ message: '请输入文章标题' })
readonly title?: string;
@ApiProperty({
description: '文章描述/简介',
example: '给你讲述美丽的大海',
required: false,
})
@IsOptional()
@IsNotEmpty({ message: '请输入文章描述' })
readonly description?: string;
@ApiProperty({
description: '文章内容',
example: '啊!美丽的大海,你是如此美丽',
required: false,
})
@IsOptional()
@IsNotEmpty({ message: '请输入文章内容' })
readonly content?: string;
}
响应
在开发的时候我们还没有定返回格式,
但是我们在第 4 节其实有定这个格式 大概格式如下:
// 列表
{
code: 200,
data: {
list: {...}
pagination: {
page: 1,
pageSize: 10,
pages: 10,
total: 100,
},
},
message: '请求成功'
}
// 详情
{
code: 200,
data: {
info: {...}
},
message: '请求成功'
}
这里我们创建几个文件
// src/modules/article/vo/article-base.vo.ts
import { ApiProperty } from '@nestjs/swagger'
class ArticleBaseItem {
@ApiProperty({ description: '文章id', example: 1 })
id: number;
@ApiProperty({ description: '创建时间', example: '2021-07-03' })
createTime: Date
@ApiProperty({ description: '更新时间', example: '2021-07-03' })
updateTime: Date
@ApiProperty({ description: '文章标题', example: '文章标题' })
title: string;
@ApiProperty({ description: '文章描述', example: '文章描述' })
description: string;
}
export class ArticleListItem extends ArticleBaseItem {}
export class ArticleInfoItem extends ArticleBaseItem {
@ApiProperty({ description: '文章内容', example: '文章内容' })
content: string;
}
// src/modules/article/vo/article-list.vo.ts
import { ApiProperty } from "@nestjs/swagger";
class SimpleInfo {
@ApiProperty({ description: '文章id', example: 1 })
id: number;
@ApiProperty({ description: '创建时间', example: '2021-07-03' })
createTime: Date
@ApiProperty({ description: '更新时间', example: '2021-07-03' })
updateTime: Date
@ApiProperty({ description: '文章标题', example: '文章标题' })
title: string;
@ApiProperty({ description: '文章描述', example: '文章描述' })
description: string;
}
class Pagination {
@ApiProperty({ description: '第几页', example: 1 })
page: number
@ApiProperty({ description: '每页条数', example: 10 })
pageSize: number
@ApiProperty({ description: '总页数', example: 10 })
pages: number
@ApiProperty({ description: '总条数', example: 100 })
total: number
}
export class ArticleListVO {
@ApiProperty({ type: SimpleInfo, isArray: true })
list: Array<SimpleInfo>
@ApiProperty({ type: () => Pagination })
pagination: Pagination
}
export class ArticleListResponse {
@ApiProperty({ description: '状态码', example: 200, })
code: number
@ApiProperty({ description: '数据', type: () => ArticleListVO, example: ArticleListVO, })
data: ArticleListVO
@ApiProperty({ description: '请求结果信息', example: '请求成功' })
message: string
}
// src/modules/article/vo/article-info.vo.ts
import { ApiProperty } from "@nestjs/swagger";
class Info {
@ApiProperty({ description: '文章id', example: 1 })
id: number;
@ApiProperty({ description: '创建时间', example: '2021-07-03' })
createTime: Date
@ApiProperty({ description: '更新时间', example: '2021-07-03' })
updateTime: Date
@ApiProperty({ description: '文章标题', example: '文章标题' })
title: string;
@ApiProperty({ description: '文章描述', example: '文章描述' })
description: string;
@ApiProperty({ description: '文章内容', example: '文章内容' })
content: string;
}
export class ArticleInfoVO {
@ApiProperty({ type: Info })
info: Info
}
export class ArticleInfoResponse {
@ApiProperty({ description: '状态码', example: 200, })
code: number
@ApiProperty({ description: '数据',
type: () => ArticleInfoVO, example: ArticleInfoVO, })
data: ArticleInfoVO
@ApiProperty({ description: '请求结果信息', example: '请求成功' })
message: string
}
我们在这两个页面里创建的每一个类,最后都会当作 swagger 的 schema,所以如果有重名的类时会导致我们的数据被覆盖(哪怕这些类不在一个文件, 所以后面我们还可以继续优化一下。
现在来改写一下 article.controller, 在方法上增加返回值的类型以及 swagger 上的响应示例
// src/modules/article/article.controller.ts
import { Controller, Body, Query, Get, Post } from '@nestjs/common';
import { ArticleService } from './article.service';
import { ArticleCreateDTO } from './dto/article-create.dto';
import { ArticleEditDTO } from './dto/article-edit.dto';
import { IdDTO } from './dto/id.dto';
import { ListDTO } from './dto/list.dto';
import { ApiTags, ApiOkResponse } from '@nestjs/swagger';
import { ArticleInfoVO, ArticleInfoResponse } from './vo/article-info.vo';
import { ArticleListResponse, ArticleListVO } from './vo/article-list.vo';
@ApiTags('文章模块')
@Controller('article')
export class ArticleController {
constructor(
private articleService: ArticleService
) {}
@Get('list')
@ApiOkResponse({ description: '文章列表', type: ArticleListResponse })
async getMore(
@Query() listDTO: ListDTO,
): Promise<ArticleListVO> {
return await this.articleService.getMore(listDTO)
}
@Get('info')
@ApiOkResponse({ description: '文章详情', type: ArticleInfoResponse })
async getOne(
@Query() idDto: IdDTO
): Promise<ArticleInfoVO>{
return await this.articleService.getOne(idDto)
}
@Post('create')
@ApiOkResponse({ description: '创建文章', type: ArticleInfoResponse })
async create(
@Body() articleCreateDTO: ArticleCreateDTO
): Promise<ArticleInfoVO> {
return await this.articleService.create(articleCreateDTO)
}
@Post('edit')
@ApiOkResponse({ description: '编辑文章', type: ArticleInfoResponse })
async update(
@Body() articleEditDTO: ArticleEditDTO
): Promise<ArticleInfoVO> {
return await this.articleService.update(articleEditDTO)
}
@Post('delete')
@ApiOkResponse({ description: '删除文章', type: ArticleInfoResponse })
async delete(
@Body() idDto: IdDTO,
): Promise<ArticleInfoVO> {
return await this.articleService.delete(idDto)
}
}
至此,我们在项目中引入了 swagger 自动生成文档的功能,同时也通过 DTO 和 Response 的类型定义了接口的传参和响应。那么在后面的接口开发中,我们就可以先定义类型,再具体实现,而且由于swagger的存在,定义好类型后,接口消费方也能方便地 mock 数据了
另外补充一点,swagger 提供 json 格式的返回,我们可以通过 swagger的json格式,轻松导入到其他接口文档工具中,如yapi、postman 等。json 格式的地址是 swagger 文档后面+ -json
如这里是
localhost:3000/swagger-doc
那么json格式就是
localhost:3000/swagger-doc-json