nestjs-swagger与请求响应配置(二)

前言

这章主要讲解 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 后续前面设置了 dtodto也可以配置上传参数的标签,以便于用户设置,再加上以前的参数验证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
相关推荐
webxue1 天前
NestJS配置环境变量、读取Yaml配置的保姆级教程
node.js·nestjs
超级无敌暴龙兽4 天前
微服务架构的基础与实践:构建灵活的分布式系统
微服务·node.js·nestjs
寻找奶酪的mouse6 天前
【NestJS全栈之旅】应用篇:通用爬虫服务三两事儿
前端·后端·nestjs
_jiang8 天前
nestjs 入门实战最强篇
redis·typescript·nestjs
敲代码的彭于晏11 天前
【Nest.js 10】JWT+Redis实现登录互踢
前端·后端·nestjs
前端小王hs24 天前
Nest通用工具函数执行顺序
javascript·后端·nestjs
明远湖之鱼1 个月前
从入门到入门学习NestJS
前端·后端·nestjs
吃葡萄不吐番茄皮1 个月前
从零开始学 NestJS(一):为什么要学习 Nest
前端·nestjs
东方小月1 个月前
Vue3+NestJS实现权限管理系统(六):接口按钮权限控制
前端·后端·nestjs
白雾茫茫丶1 个月前
Nest.js 实战 (十四):如何获取客户端真实 IP
nginx·nestjs