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
相关推荐
Eric_见嘉1 天前
NestJS 🧑‍🍳 厨子必修课(九):API 文档 Swagger
前端·后端·nestjs
XiaoYu20029 天前
第3章 Nest.js拦截器
前端·ai编程·nestjs
XiaoYu200211 天前
第2章 Nest.js入门
前端·ai编程·nestjs
实习生小黄11 天前
NestJS 调试方案
后端·nestjs
当时只道寻常14 天前
NestJS 如何配置环境变量
nestjs
濮水大叔1 个月前
VonaJS是如何做到文件级别精确HMR(热更新)的?
typescript·node.js·nestjs
ovensi1 个月前
告别笨重的 ELK,拥抱轻量级 PLG:NestJS 日志监控实战指南
nestjs
ovensi1 个月前
Docker+NestJS+ELK:从零搭建全链路日志监控系统
后端·nestjs
Gogo8161 个月前
nestjs 的项目启动
nestjs