前言
这章主要讲解 swagger 的配置,输入输出结构配置等,也顺道会加入一些返回的请求格式的案例,方便参考
swagger配置
swagger配置一些会跟其前面介绍的一样,只是更加详细一些
安装swagger
先使用 npm、yarn 导入 swagge相关
yarn add @nestjs/swagger swagger-ui-express
配置main函数
然后再 main函数开启文档,当我们项目运行的时候,文档就能看到了
const options = new DocumentBuilder()
.setTitle('nest demo api')
.setDescription('This is nest demo api')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api-docs', app, document);
//为了避免正式服务器暴漏文档的情况,可以设置正式服务器 swaggerUiEnabled 设置false,则就不显示
//或者部署到正式服务器是,直接删除文档即可,直接写到环境变量动态控制也挺好
//SwaggerModule.setup('api-docs', app, document, {
// swaggerUiEnabled: envConfig.SWAGGER_UI_ENABLE ? true : false,
//});
//这个地址本地看得话就是 http://localhost:3000/api-docs了
main 函数配置的就是,swagger顶部那些,如下所示,下面的也会介绍
配置路由文档
配置模块名称 @ApiTags,也就是给一个模块添加一个大标题索引,方便快速区分api功能的
@ApiTags('user')
@Controller('user')
export class UserController {
......
}
配置接口路由备注 @ApiOperation,可以设置单个接口备注
配置 get 请求时, Query 类型,在 swagger 中也会默认为必填,我们也可以通过
// 使用query类型
// .../api?id=value&...
@ApiOperation({
summary: '获取用户信息'
})
@Get('getUserInfo') //命名追加到url路径上
getUserInfo(
@Query('id')
// @Query('id', new ParseIntPipe()) //也可以通过 pipe 校验类型,如果不是int类型会报错
id: number //声明一个 query 类型 id,类型为number
) {
...
}
post接口需要用到 dto,配置一下 dto校验 和 文档
@ApiOperation({
summary: '修改用户信息2',
})
@Post('updateUserInfo')
updateUserInfo(//可以获取headers中的内容,例如版本号平台
@Body() userInfo: UserDto
) {
......
}
配置body的dto文档
前面给 @Body 后续前面设置了 dto,dto也可以配置上传参数的标签,以便于用户设置,再加上以前的参数验证pipe,如下所示
下面 description参数描述, example 为参数案例,@ApiProperty表示必填,@ApiPropertyOptional表示可选属性
export class UserDto {
//api属性备注,必填
@ApiProperty({description: '名字', example: '迪丽热巴'})
//设置了 IsNotEmpty 就是必填属性了,文档也会根据该验证来显示是否必填
@IsNotEmpty({ message: 'name不能为空' })//可以返回指定message,返回为数组
// @IsNotEmpty()//返回默认message,默认为为原字段的英文提示
readonly name: string
//可选参数
@ApiPropertyOptional({description: '年龄', example: 20})
readonly age: number
@ApiPropertyOptional({description: '手机号', example: '133****3333'})
readonly mobile: string
@ApiPropertyOptional({description: '性别 1男 2女 0未知', example: 1})
@Max(2)
@Min(0)
readonly sex: number
@ApiPropertyOptional({description: '是否已婚', example: false})
@IsNotEmpty()
marry: number
}
body 传参如下所示,可以点击 schema 查看必填项
其他校验和自定义校验
校验用的是这个 class-validator 仓库 , 有疑问的可以看看
我们常见的装饰器 @IsNotEmpty、@IsNumber、@MaxLength、@Max、@IsString 等等如果缺少的话,可以进入文档,或者随便一个装饰器点进去,看顶部有哪些可以用就可以了
ps:@IsOptional可选类型,平时用的不多,默认可选,当存在多个校验时,如果可选则需要额外标记,不然不传也会报错误
//设置不为空
@IsNotEmpty()
account: string
//设置年龄范围
@Max(35)
@Min(20)
work_age: number
找装饰器的时候,如果忘了一些,懒得看文档,直接随便点一个校验装饰及,然后目录找,这样也能最快找到我们用的
上面那些装饰器,虽然很实用,但一些特殊的,我们仍然无法实现效果,例如:我要设置一个金额校验,最多保留小数两位小数,下面直接实现以下
//模仿一个,需要实现 ValidatorConstraintInterface 接口,类名我们需要主动使用时当做类型传递
@ValidatorConstraint()
export class IsMoney implements ValidatorConstraintInterface {
//第一个参数是接收到的我们的参数,第二个是validate的三个参数中第二个传递的(为数组类型)
validate(text: string, validationArguments: ValidationArguments) {
return typeof text === 'string' &&
/(^\d{1}$|^[1-9]\d+$)|(^[1-9]\d*|^0)\.\d{1,2}$/.test(text)
}
defaultMessage(args: ValidationArguments) {
//也可以使用带属性命的来提示默认效果
return `${args.property}不是金额类型字符串`;
}
}
//设置价格类型,如果还需要参数,则可以在第二个参数中传递数组,上面的ValidationArguments可以访问到
//如果想设置新的提示,可以第二个或者第三个参数传递对象 {message: "新的提示"}
@Validate(IsMoney)
//@Validate(IsMoney, { message: '新的提示'})
price: string
配置返回格式swagger
正常我们不会用系统的状态码显示,而是配置成下面的样式,data 字段我们则是灵活的
//对象类型data
{
"code": 200,
"msg": "ok",
"data": {
"name": "西瓜",
"age": 20,
"mobile": "133****3333",
"sex": 1,
"marry": false
}
}
//数组类型data
{
"code": 200,
"msg": "ok",
"data": [
{
"name": "甜瓜",
"age": 20,
"mobile": "133****3333",
"sex": 1,
"marry": false
}
]
}
//page页的长列表
{
"code": 200,
"msg": "ok",
"data": {
"items": [
{
"name": "哈密瓜",
"age": 20,
"mobile": "133****3333",
"sex": 1,
"marry": false
}
],
"itemCount": 0,
"totalItems": 0,
"totalPages": 0,
"currentPage": 0,
"itemsPerPage": 0
}
}
先编写一个新的装饰器,系统有一个 ApiResponse,不好用,我们使用自己创建的,取名 APIResponse,如下所示
import { Type } from '@nestjs/common'
import { ApiResponse, getSchemaPath } from '@nestjs/swagger'
const baseTypeNames = ['String', 'Number', 'Boolean']
/**
* @description: 生成返回结果装饰器
*/
export const APIResponse = <TModel extends Type<any>>(
type?: TModel | TModel[],
isPage?: boolean
) => {
let prop = null
if (Array.isArray(type)) {
if (isPage) {
prop = {
type: 'object',
properties: {
items: {
type: 'array',
items: { $ref: getSchemaPath(type[0]) },
},
itemCount: { type: 'number', default: 0 },
totalItems: { type: 'number', default: 0 },
totalPages: { type: 'number', default: 0 },
currentPage: { type: 'number', default: 0 },
itemsPerPage: { type: 'number', default: 0 },
},
}
} else {
prop = {
type: 'array',
items: { $ref: getSchemaPath(type[0]) },
}
}
} else if (type) {
if (type && baseTypeNames.includes(type.name)) {
prop = { type: type.name.toLocaleLowerCase() }
} else {
prop = { $ref: getSchemaPath(type) }
}
} else {
prop = { type: 'null', default: null }
}
let resProps = {
type: 'object',
properties: {
code: { type: 'number', default: 200 },
msg: { type: 'string', default: 'ok' },
data: prop
},
}
return applyDecorators(
ApiExtraModels(type ? (Array.isArray(type) ? type[0] : type) : String),
ApiResponse({
schema: {
allOf: [
resProps
],
},
}),
)
}
装饰器使用如下所示
//单个类
...
@APIResponse(UserDto)
updateUserInfo(
...
) {
...
//使用findOne获取一个user
return ResponseData.ok(user);
}
//数组类型,一个普通的元组即可
...
@APIResponse([UserDto])
updateUserInfo(
...
) {
...
//使用find获取多个users
return ResponseData.ok(users);
}
//长列表,带page页的
...
@APIResponse([UserDto], true)
updateUserInfo(
...
) {
...
//使用findAndCount 可以获取数据和总数量, userPage
return ResponseData.pageOk(userPage, pageDto); //用后面的类即可
}
配置响应类
上面的只是配置了一个 swagger,实际使用如果不按照固定格式返回也是不友好的
同时上一篇文章也提到了,如果用全局拦截,除了状态码用的不太舒服,还有就是感觉逻辑不是很清晰,一股大杂烩的感觉,下面配置一下相对比较好用的格式
import { PageDto } from "./page.dto"
export interface PageItem {
totalPages?: number
itemCount?: number
currentPage?: number
itemsPerPage?: number
totalItems?: number
}
export interface ReponsePage<T> extends PageItem {
items: T[]
}
export class ResponseData<T> {
code: number; //状态码
msg: string; //消息
data?: T; //数据内容
constructor(code = 200, msg: string, data: T = null) {
this.code = code;
this.msg = msg;
this.data = data;
}
static ok<T>(data: T = null, message = 'ok'): ResponseData<T> {
return new ResponseData(200, message, data);
}
static fail(message = 'fail', code = -1): ResponseData<null> {
return new ResponseData(code, message);
}
//page直接使用 findAndCount + PageDto,直接解决
static pageOk<T>(data: [T[], number] = [[], 0], page: PageDto, message = 'ok'): ResponseData<ReponsePage<T>> {
let items = data[0]
let totolCount = data[1]
return new ResponseData(200, message, {
items: items, //数据
totalItems: totolCount,
currentPage: page.page_num,
itemsPerPage: page.page_size,
itemCount: items.length,
totalPages: Math.ceil(totolCount / page.page_size),
});
}
}
编写一个 PageDto 方便分页使用,会方便很多,后续查询会看到用着很舒服,具备分页特性的一般继承 PageDto 即可
import { ApiPropertyOptional } from '@nestjs/swagger';
export const defaultPageNum = 1; //默认第一页码
export const defaultPageSize = 10; //每页默认数量
export class PageDto {
@ApiPropertyOptional({ description: '页码 1 开始', example: 1 })
page_num: number
@ApiPropertyOptional({ description: '每页数量', example: 10 })
page_size: number
get skip() {
return (this.page_num - 1) * this.page_size;
}
get take() {
return this.page_size;
}
constructor(page: PageDto) {
//已经声明数字为何还要转化,因为前端传递过来的基本都是字符串,虽然实际查询等写入操作不影响,但实际操作和返回内容格式化时依照实际类型会出现异常(ts只是开发阶段规范,实际运行还是js),因此需要转化成数字,能避免一些问题
this.page_num =
(page?.page_num && parseInt(page.page_num + '')) || defaultPageNum;
this.page_size =
(page?.page_size && parseInt(page.page_size + '')) ||
defaultPageSize;
}
}
这样就完成了,设置到controller中吧
@Post('updateUserInfo')
@APIResponse(UserDto) //返回空
@APIResponse(UserDto) //返回一个基础类
@APIResponse([UserDto])//返回一个数组
@APIResponse([UserDto], true)//返回一个page类型的数组
updateUserInfo(
@Body() userInfo: UserDto,
) {
//实际逻辑应该写到 service 中
return ResponseData.ok(userInfo)
}
配置嵌套类
如下所示配置一个可以嵌套的,其中包含对象和数字的,如下所示
//配置一个专栏,包含嵌套类,如下所示
export class FeatureDto {
@ApiProperty({ description: 'id', example: 1 })
id: number
@ApiProperty({ description: '名称', example: '1231' })
name: string
//专栏状态,默认创建即送审,等待、审核中、成功、失败
//平时可以数字或者单个字符,以提升实际效率和空间,文档注释最重要,这里纯粹为了看着清晰
@ApiProperty({ description: '状态', example: 1 })
status: FeatureStatus
@ApiProperty({ description: '用户id', example: 1 })
userId: number
//专栏拥有者
@ApiProperty({ description: '用户信息', type: () => UserDto, example: UserDto })
//@ApiProperty({ description: '用户信息', type: () => UserDto, required: true, })
user: UserDto
//数组的类型要设置成数组,案例设置成对象即可显示一个对象
@ApiProperty({ description: '文章列表', type: () => [ArticleDto], example: ArticleDto })
articles: ArticleDto[]
@ApiProperty({ description: '订阅列表', type: () => [UserDto], example: UserDto })
subscribes: UserDto[]
}
查看返回结果
生成一个数据看看吧,挺成功的
{
"code": 200,
"msg": "ok",
"data": {
"id": 1,
"name": "1231",
"status": 1,
"userId": 1,
"user": {
"nickname": "小鬼快跑",
"age": 20,
"mobile": "133****3333",
"sex": 1
},
"articles": [
{
"id": 1,
"title": "标题",
"desc": "描述",
"content": "内容",
"status": 0,
"createTime": "2022",
"updateTime": "2022",
"user": {
"nickname": "小鬼快跑",
"age": 20,
"mobile": "133****3333",
"sex": 1
},
"userId": 1,
"collectCount": 10,
"featureId": 10
}
],
"subscribes": [
{
"nickname": "小鬼快跑",
"age": 20,
"mobile": "133****3333",
"sex": 1
}
]
}
分页的数据是这样的
{
"code": 200,
"msg": "ok",
"data": {
"items": [
{
"id": 1,
"title": "标题",
"desc": "描述",
"content": "内容",
"status": 0,
"createTime": "2022",
"updateTime": "2022",
"user": {
"nickname": "小鬼快跑",
"age": 20,
"mobile": "133****3333",
"sex": 1
},
"userId": 1,
"collectCount": 10,
"featureId": 10
}
],
"itemCount": 0,
"totalItems": 0,
"totalPages": 0,
"currentPage": 0,
"itemsPerPage": 0
}
}
配置自定义返回类(针对数组文档不正常)
配置上面类的时候非常简单,然后有时候我们也会遇到数组,遇到略微自定义的复杂返回结果,此时如果使用原来的可能会遇到问题,主要是针对于数组类型,需要将 example 改成 examples,也可以直接 required: true
## 错误示范
//一个自定义类型的返回结果,包含数组
export class ArticleCollectsAndOthersDto {
@ApiProperty({
description: '收藏文章信息',
type: () => [ArticleDto],
example: [ArticleDto],
})
collects: ArticleDto[]
@ApiProperty({
description: '我自己文章信息',
type: () => [ArticleDto],
example: [ArticleDto],
})
others: ArticleDto[]
}
如果此种情况按照上面写,会发现文档里面会显示一个数组,里面都是 null,即格式为 [null],原因是参数使用错误,下面我纠正一下,只需要将 example 改成 examples 即可,这样就是我们想要的文档了
注意:除了这种会出现数组,我们多对多或者其他类型也会出现数组,文档格式稍微动一下就行了,没必要为了一个文档强行单独转化成 json 字符串返回
## 正确示范
//一个自定义类型的返回结果,包含数组
export class ArticleCollectsAndOthersDto {
@ApiProperty({
description: '收藏文章信息',
type: () => [ArticleDto],
examples: [ArticleDto],
})
collects: ArticleDto[]
@ApiProperty({
description: '我自己文章信息',
type: () => [ArticleDto],
examples: [ArticleDto],
})
others: ArticleDto[]
}
也可以直接统一required: true,我看好像都没问题
export class ArticleCollectsAndOthersDto {
@ApiProperty({
description: '收藏文章信息',
type: () => [ArticleDto],
required: true,
})
collects: ArticleDto[]
@ApiProperty({
description: '我自己文章信息',
type: () => [ArticleDto],
required: true,
})
others: ArticleDto[]
}
使用 OmitType、PickType 去除、取出参数生成新的dto
在创建 dto 的时候,很多时候我们都是以详情为基准创建,但是列表页有时候会因为性能把不必要的关联参数去掉,因此会需要丢失一些参数,但是文档使用详情的 dto 会多余,重新配置文档还要复制粘贴会多很多不必要的代码,因此 ts 对应的高级语法,在 nest-swagger 中也有体现,例如: OmitType 则可以去掉多余字段,跟 Omit 类似,PickType可以去除一些字段,和 Pick类似 都可以被继承
//例如:我们省略掉 FeatureDto 中的 articles、subscribes,作为List表示
export class FeatureListDto extends OmitType(FeatureDto, ['articles', 'subscribes']) {}
//我们取出里面的几个字段作为我们的新的dto
export class TemplateDto extends PickType(TemplateBaseDto, ['id', 'name', 'status']) {}
这样就好了,还有其他的例如: PartialType 为什么不用呢,因为一些额外的修改会去掉文档注释,因此不推荐(不需要文档那无所谓了)
设置api路由前缀
有时为了使接口api更加清晰化,或预留位置等情况,我们开发时,会给我们的项目添加一个全局路由
js
app.setGlobalPrefix('api');
这样接口的基础地址就会变成 http://localhost:3000/api
文档还是原来那个不受影响 /api-docs
设置参数映射swagger
正常开发时,如果比较忙,我们可能并不是所有的参数都需要写文档,有些参数根据名字就立马能知道啥意思,因此只需要把关键点给注释上即可,我们可以通过 设置 nest-cli.json 文件,添加插件即可实现参数映射到swagger文档,这样即使没有写 ApiProperty 文档也会出现参数了,写了会出现我们的注释
//默认的是这样子的
"compilerOptions": {
"deleteOutDir": true,
}
//我们添加一个 @nestjs/swagger 映射
"compilerOptions": {
"deleteOutDir": true,
"plugins": [{
"name": "@nestjs/swagger",
"options": {
"classValidatorShim": false,
"introspectComments": true
}
}]
}
ps:这个映射会将我们controller的对象参数映射出来,其他文章使用headers装饰器的参数也会全部应用上(这样就会多出来很多无用的参数校验文档),如果用了为了方便可以放到request里面,自定义一个子类接受即可