NestJs 手摸手之《响应封装》

1,640 阅读3分钟

介绍

NestJs 接口统一响应封装,支持成功、错误、分页响应,封装了Swagger装饰器更友好的生成接口文档

⚠️阅前请先学习Swagger基础!

1. 定义响应码、响应信息

src/const/api.const.ts

export const API_CODES = {
  OK: 200,
  UNKNOWN: 99999,
  USER_EXIST: 40001,
  USER_NO_EXIST: 40002,
};

export const API_MSGS = {
  [API_CODES.OK]: '成功',
  [API_CODES.UNKNOWN]: '未知错误',
  [API_CODES.USER_EXIST]: '用户已存在',
  [API_CODES.USER_NO_EXIST]: '用户不存在',
};

2. 定义响应数据类

定义响应数据类,并使用@ApiProperty装饰器对属性进行描述,用于Swagger自动生成接口文档

src/dto/api.dto.ts

⚠️文件命名必须.dto.ts结尾,否则Swagger-cli将无法解析

import { API_CODES, API_MSGS } from '../const/api.const';
import { ApiProperty } from '@nestjs/swagger';

/**
 * 基础响应类
 */
export class ApiBaseRes {
  @ApiProperty({ description: '状态码' })
  code: number;
  @ApiProperty({ description: '状态信息' })
  msg: string;
}

/**
 * 基础Ok响应类
 */
export class ApiBaseOkRes extends ApiBaseRes {
  @ApiProperty({ default: API_CODES.OK })
  code: number;
  @ApiProperty({ default: API_MSGS[API_CODES.OK] })
  msg: string;
}

/**
 * Ok响应类
 */
export class ApiOkRes<TData = any> extends ApiBaseOkRes {
  @ApiProperty({ description: '数据' })
  data: TData;
}

/**
 * Err响应类
 */
export class ApiErrRes extends ApiBaseRes {
  @ApiProperty({ description: '错误详细' })
  err?: string;
}

/**
 * 分页Ok响应类
 */
export class ApiPagerOkRes<TData = any> extends ApiBaseOkRes {
  @ApiProperty({ description: '分页页码', default: 1 })
  page: number;
  @ApiProperty({ description: '分页页大小', default: 10 })
  limit: number;
  @ApiProperty({ description: '分页数据总量', default: 10 })
  total: number;
  @ApiProperty({ description: '分页列表' })
  list: TData[];
}

/**
 * 分页输入参数类
 */
export class PagerDto {
  @ApiProperty({ description: '分页页码', example: 1 })
  page: number;
  @ApiProperty({ description: '分页页大小', example: 10 })
  limit: number;
}

/**
 * 空模型类
 */
export class EmptyModel {}

3. 封装响应

src/utils/api.ts

import { API_MSGS, API_CODES } from '../const/api.const';
import { ApiErrRes, ApiOkRes, ApiPagerOkRes } from 'src/dto/api.dto';

interface IPagerOkOpts {
  list: any[];
  page: number;
  limit: number;
  total: number;
}
type IErrArgs = [number, string?, Error?];

/**
 * 成功
 */
export class ApiOk implements ApiOkRes {
  code: number = API_CODES.OK;
  msg: string = API_MSGS[API_CODES.OK];
  data: any;
  constructor(data: any = null) {
    this.data = data;
  }
}

/**
 * 分页成功
 */
export class ApiPagerOk implements ApiPagerOkRes {
  code: number = API_CODES.OK;
  msg: string = API_MSGS[API_CODES.OK];
  list: any;
  page: number;
  limit: number;
  total: number;
  constructor({ list = [], page, limit: pageSize, total }: IPagerOkOpts) {
    this.list = list;
    this.page = page;
    this.limit = pageSize;
    this.total = total;
  }
}

/**
 * 错误
 */
export class ApiErr implements ApiErrRes {
  code: number = API_CODES.UNKNOWN;
  msg: string = API_MSGS[API_CODES.UNKNOWN];
  err?: string;

  constructor(...args: IErrArgs) {
    const [code, msg, err] = args;
    this.code = code;
    this.msg = msg ?? API_MSGS[code];
    this.err = err ? err.toString() : null;
  }
}

/**
 * Api响应类
 */
export class Api {
  /**
   * 成功
   */
  static ok = (data?) => new ApiOk(data);

  /**
   * 分页成功
   */
  static pagerOk = (opts: IPagerOkOpts) => new ApiPagerOk(opts);

  /**
   * 错误
   */
  static err = (...args: IErrArgs) => new ApiErr(...args);
}

4. 配置Swagger

在nest-cli.json中添加如下配置

选项描述
dtoFileNameSuffixDTO(数据传输对象)文件后缀
classValidatorShim如果设置为 true,模块将重用class-validator验证装饰器(例如@Max(10),将添加max: 10到模式定义中)
introspectComments如果设置为 true,插件将根据评论为属性生成描述和示例值

nest-cli.json

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "plugins": [
      {
        "name": "@nestjs/swagger",
        "options": {
          "dtoFileNameSuffix": [
            ".dto.ts",
            ".entity.ts",
            ".interface.ts",
            ".schema.ts"
          ],
          "classValidatorShim": true,
          "introspectComments": true
        }
      }
    ]
  }
}

5. 封装Swagger装饰器

src/decorators/swagger.decorator.ts

import {
  getSchemaPath,
  ApiExtraModels,
  ApiResponse,
  ApiBody,
} from '@nestjs/swagger';
import { applyDecorators, Type } from '@nestjs/common';
import {
  ApiErrRes,
  ApiOkRes,
  ApiPagerOkRes,
  EmptyModel,
  PagerDto,
} from 'src/dto/api.dto';
import { API_CODES, API_MSGS } from 'src/const/api.const';

/**
 * 成功
 */
export const SwaggerOk = <TModel extends Type<any>>(model?: TModel) => {
  const decorators = [
    ApiExtraModels(ApiOkRes),
    ApiExtraModels(model ?? EmptyModel),
    ApiResponse({
      description: API_MSGS[API_CODES.OK],
      status: API_CODES.OK,
      schema: {
        allOf: [
          { $ref: getSchemaPath(ApiOkRes) },
          {
            properties: {
              data: {
                $ref: getSchemaPath(model ?? EmptyModel),
                default: null,
              },
            },
          },
        ],
      },
    }),
  ];

  return applyDecorators(...decorators);
};

/**
 * 分页成功
 */
export const SwaggerPagerOk = <TModel extends Type<any>>(model: TModel) => {
  const decorators = [
    ApiExtraModels(ApiPagerOkRes),
    ApiExtraModels(model),
    ApiResponse({
      description: API_MSGS[API_CODES.OK],
      status: API_CODES.OK,
      schema: {
        allOf: [
          { $ref: getSchemaPath(ApiPagerOkRes) },
          {
            properties: {
              list: {
                type: 'array',
                items: { $ref: getSchemaPath(model) },
              },
            },
          },
        ],
      },
    }),
  ];
  return applyDecorators(...decorators);
};

/**
 * 错误
 */
export const SwaggerErr = (code) => {
  const decorators = [
    ApiExtraModels(ApiErrRes),
    ApiResponse({
      description: `${API_MSGS[code]}`,
      status: code,
      schema: {
        allOf: [
          { $ref: getSchemaPath(ApiErrRes) },
          {
            properties: {
              code: {
                default: code,
              },
              msg: {
                default: API_MSGS[code],
              },
              err: {
                default: null,
              },
            },
          },
        ],
      },
    }),
  ];
  return applyDecorators(...decorators);
};

/**
 * 分页入参
 */
export const SwaggerPagerBody = <TModel extends Type<any>>(model?: TModel) => {
  const decorators = [
    ApiExtraModels(PagerDto),
    ApiExtraModels(model ?? EmptyModel),
    ApiBody({
      schema: {
        allOf: [
          { $ref: getSchemaPath(PagerDto) },
          { $ref: getSchemaPath(model ?? EmptyModel) },
        ],
      },
    }),
  ];
  return applyDecorators(...decorators);
};

6. 定义interface

此处的注释必须使用此格式,否则Swagger无法生成description

src/modules/user/user.interface.ts

export class IUser {
  /**
   * 账号
   */
  account: string;
  /**
   * 昵称
   */
  nickname: string;
  /**
   * 年龄
   */
  age: number;
}

7. 使用

src/modules/user/user.controller.ts

import { API_CODES } from 'src/const/api.const';
import { IUser } from './user.interface';
import { Body, Controller, Post } from '@nestjs/common';
import { UserService } from './user.service';
import { UserInfoDto } from './user.dto';
import {
  SwaggerErr,
  SwaggerOk,
  SwaggerPagerOk,
} from 'src/decorators/swagger.decorator';

@Controller('user')
export class UserController {
  constructor(private userService: UserService) {}

  @Post('info')
  @SwaggerOk(IUser)
  @SwaggerErr(API_CODES.USER_NO_EXIST)
  info(@Body() userInfoDto: UserInfoDto) {
    return this.userService.info(userInfoDto);
  }

  @Post('list')
  @SwaggerPagerOk(IUser)
  list() {
    return this.userService.list();
  }
}

src/modules/user/user.service.ts

import { Injectable } from '@nestjs/common';
import { API_CODES } from 'src/const/api.const';
import { Api } from 'src/utils/api';
import { UserInfoDto } from './user.dto';
import { IUser } from './user.interface';

@Injectable()
export class UserService {
  private dbUsers: IUser[] = [
    {
      account: '111111',
      nickname: 'cbb',
      age: 18,
    },
    {
      account: '222222',
      nickname: 'cbb',
      age: 18,
    },
    {
      account: '333333',
      nickname: 'cbb',
      age: 18,
    },
    {
      account: '444444',
      nickname: 'cbb',
      age: 18,
    },
    {
      account: '555555',
      nickname: 'cbb',
      age: 18,
    },
  ];

  /**
   * 查询用户信息
   */
  info(userInfoDto: UserInfoDto) {
    const user = this.dbUsers.find(
      (user) => user.account === userInfoDto.account,
    );

    if (user) {
      // 成功 - 查询到用户
      return Api.ok(user);
    } else {
      // 错误 - 查询的用户不存在
      return Api.err(API_CODES.USER_NO_EXIST);
    }
  }

  /**
   * 用户列表
   */
  list() {
    return Api.pagerOk({
      list: this.dbUsers,
      page: 1,
      limit: 10,
      total: this.dbUsers.length,
    });
  }
}
}

完整示例代码

github.com/cbingb666/N…