前言
这章主要讲解 swagger 的配置,输入输出结构配置等,也顺道会加入一些返回的请求格式的案例,方便参考
swagger配置
swagger配置一些会跟其前面介绍的一样,只是更加详细一些
安装swagger
先使用 npm、yarn
导入 swagge
相关
sql
js
yarn add @nestjs/swagger swagger-ui-express
配置main函数
然后再 main
函数开启文档,当我们项目运行的时候,文档就能看到了
javascript
js
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);
//这个地址本地看得话就是 http://localhost:3000/api-docs了
main 函数配置的就是,swagger顶部那些,如下所示,下面的也会介绍
配置路由文档
配置模块名称 @ApiTags
,也就是给一个模块添加一个大标题索引,方便快速区分api功能的
kotlin
js
@ApiTags('user')
@Controller('user')
export class UserController {
......
}
配置接口路由备注 @ApiOperation
,可以设置单个接口备注
配置 get
请求时, Query
类型,在 swagger
中也会默认为必填,我们也可以通过
js
// 使用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校验 和 文档
less
js
@ApiOperation({
summary: '修改用户信息2',
})
@Post('updateUserInfo')
updateUserInfo(//可以获取headers中的内容,例如版本号平台
@Body() userInfo: UserDto
) {
......
}
配置body的dto文档
前面给 @Body
后续前面设置了 dto
,dto
也可以配置上传参数的标签,以便于用户设置,再加上以前的参数验证pipe,如下所示
下面 description
参数描述, example
为参数案例,@ApiProperty
表示必填,@ApiPropertyOptional
表示可选属性
js
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})
@Validate((val: number) => !val || (val > 0 && val <= 1))
readonly sex: number
@ApiPropertyOptional({description: '是否已婚', example: false})
@IsNotEmpty()
marry: number
}
body
传参如下所示,可以点击 schema
查看必填项
配置返回格式swagger
正常我们不会用系统的状态码显示,而是配置成下面的样式,data 字段我们则是灵活的
js
//对象类型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
,如下所示
js
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
],
},
}),
)
}
装饰器使用如下所示
js
//单个类
...
@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,实际使用如果不按照固定格式返回也是不友好的
同时上一篇文章也提到了,如果用全局拦截,除了状态码用的不太舒服,还有就是感觉逻辑不是很清晰,一股大杂烩的感觉,下面配置一下相对比较好用的格式
js
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 即可
js
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) {
this.page_num = page?.page_num || defaultPageNum;
this.page_size = page?.page_size ? page.page_size : defaultPageSize;
}
}
这样就完成了,设置到controller中吧
js
@Post('updateUserInfo')
@APIResponse(UserDto) //返回空
@APIResponse(UserDto) //返回一个基础类
@APIResponse([UserDto])//返回一个数组
@APIResponse([UserDto], true)//返回一个page类型的数组
updateUserInfo(
@Body() userInfo: UserDto,
) {
//实际逻辑应该写到 service 中
return ResponseData.ok(userInfo)
}
配置嵌套类
如下所示配置一个可以嵌套的,其中包含对象和数字的,如下所示
js
//配置一个专栏,包含嵌套类,如下所示
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 })
user: UserDto
//数组的类型要设置成数组,案例设置成对象即可显示一个对象
@ApiProperty({ description: '文章列表', type: () => [ArticleDto], example: ArticleDto })
articles: ArticleDto[]
@ApiProperty({ description: '订阅列表', type: () => [UserDto], example: UserDto })
subscribes: UserDto[]
}
查看返回结果
生成一个数据看看吧,挺成功的
js
{
"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
}
]
}
分页的数据是这样的
js
{
"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
}
}
设置api路由前缀
有时为了使接口api更加清晰化,或预留位置等情况,我们开发时,会给我们的项目添加一个全局路由
bash
js
app.setGlobalPrefix('api');
这样接口的基础地址就会变成 http://localhost:3000/api
文档还是原来那个不受影响 /api-docs