NestJS 搭建博客系统(六)— 使用 Swagger 生成文档

5,119 阅读6分钟

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

参考

系列