接口包装:将return的数据按约定好的结构进行返回

{
  "code": 0,
  "msg": "success",
  "data": "Hello world"
}

异常处理:按约定好的结构,返回业务状态码和异常messge

{
  "code": 4002,
  "msg": "手机号长度错误"
}

业务状态码

常见的第三方接口,会带上自己定义的业务状态码,举一个简单的例子

// enums/api-code.enums.ts
export enum ApiCode {
  TIMEOUT = -1, // 系统繁忙
  SUCCESS = 0, // 请求成功

  BUSINESS_ERROR = 4001, // 业务错误
  PARAMS_ERROR = 4002, // 参数不合法
  SIGN_ERROR = 4003, // 验签失败
  TOKEN_ERROR = 4004, // token不合法
}

接口包装

@Get()
async getString(): Promise<string> {
  return 'Hello world'
}

默认情况下,客户端拿到的就只有'Hello world'字符串,但是我们不可能在每个controller里都写一遍包装的逻辑再return掉,这时候就需要拦截器替我们自动包装了。

自定义拦截器需要实现NestInterceptor接口,定义如下:

export interface NestInterceptor<T = any, R = any> {
  intercept(context: ExecutionContext, next: CallHandler<T>): Observable<R> | Promise<Observable<R>>;
}

ExecutionContext接口定义:

export interface ExecutionContext extends ArgumentsHost {
  getClass<T = any>(): Type<T>;
  getHandler(): Function;
}

ExecutionContext继承自ArgumentsHostArgumentsHost则是对上下文的包装(其实是个数组):如果是HTTP应用,那么就是[request, response];如果是Web Socket应用,那么就是[client, data]

ExecutionContext则添加了两种方法,getHandler获取的是处理响应的Handler(比如上文的getString);getClass获取的是Hanlder所属的Controller类(不是实例)

再看一下CallHandler的接口定义

export interface CallHandler<T = any> {
  handle(): Observable<T>;
}

它的作用很简单,调用handle时返回订阅的ObservableObservableRxJS相关概念,不展开描述,它的核心是流和操作符(用起来其实很方便,但是理解真的很抽象)

回到实际场景,新建拦截器providers/interceptor/api.interceptor.ts

// providers/interceptor/api.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { ApiCode } from '../../enum/api-code.enum'

// 约定好的返回格式
interface Response<T> {
  code: number
  msg: string
  data: T
}

@Injectable()
export class ApiInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(
    context: ExecutionContext,
    next: CallHandler
  ): Observable<Response<T>> {
    return next.handle().pipe(
      map(data => {
        return {
          code: ApiCode.SUCCESS,
          msg: 'success',
          data
        }
      })
    )
  }
}

这样子一个简单的接口包装就完成了,由于我们对每一个响应都需要包装,就把它注册为全局拦截器:

// app.module.ts
import { Module } from '@nestjs/common'
import { APP_INTERCEPTOR } from '@nestjs/core'

@Module({
  providers: [
    { provide: APP_INTERCEPTOR, useClass: ApiInterceptor }
  ]
})
export class ApplicationModule {}

完成。

异常包装

上面针对的是正常返回(return)的情况,在实际业务时会有各类异常情况出现,按照最直接的方法来的话(不含上述拦截器):

// user.controller.ts
@Get(':id')
async getUser(@Res() res, @Param('id', new ParseIntPipe()) id) {
  if(isNaN(id)) {
    return res.status(HttpStatus.OK).send({
      code: 4002,
      msg: 'id不合法'
    })
  }

  return res.status(HttpStatus.OK).send({
    code: 0,
    msg: 'success',
    data: { name: 'a', age: 18 }
  })
}

这种写法会带来耦合度高、代码冗余等问题,且和刚才写的接口包装拦截器有冲突,那么这些异常该如何优雅地进行返回呢?

Nest会对应用抛出的异常进行捕获,生成一个JSON返回,比如访问一个不存在的path:

{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Cannot GET /member"
}

因此我们只需要抛出异常就行了,正常返回也可以通过拦截器进行包装:

// user.controller.ts
@Get(':id')
async getUser(@Res() res, @Param('id', new ParseIntPipe()) id) {
  if(isNaN(id)) {
    throw new HttpException('ID不合法', HttpStatus.OK)
  }

  return { name: 'a', age: 18 }
}

但是这样返回的异常并不符合我们需要的结构,我们需要自定义一个异常过滤器来处理异常的返回。

在这之前,我们先自定义一个异常,把我们的业务码放进去:

// api.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common'
import { ApiCode } from '../../enum/api-code.enum'

export class ApiException extends HttpException {
  private errorMessage: string
  private errorCode: ApiCode

  constructor(
    errorMessage: string,
    errorCode: ApiCode,
    statusCode: HttpStatus
  ) {
    super(errorMessage, statusCode)

    this.errorMessage = errorMessage
    this.errorCode = errorCode
  }

  gerErrorCode(): ApiCode {
    return this.errorCode
  }

  getErrorMessage(): string {
    return this.errorMessage
  }
}

自定义过滤器需要实现ExceptionFilter接口:

export interface ExceptionFilter<T = any> {
  catch(exception: T, host: ArgumentsHost): any;
}

exception是捕获到的异常,hostArgumentsHost实例,上文有提到。

然后写我们需要的自定义过滤器:

// http-exception.filter.ts 
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException
} from '@nestjs/common'
import { ApiException } from '../exception/api.exception'

// 该装饰器告诉filter要捕获哪些类型异常
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception, host: ArgumentsHost): void {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse()
    const request = ctx.getRequest()
    const status = exception.getStatus()

    if (exception instanceof ApiException) {
      response.status(status).json({
        msg: (exception as ApiException).getErrorMessage(),
        code: (exception as ApiException).gerErrorCode(),
        path: request.url
      })
    } else {
      // 处理非手动抛出的情况
      response.status(status).json((exception as HttpException).getResponse())
    }
  }
}

最后把过滤器也注册到全局上:

// app.module.ts
import { Module } from '@nestjs/common'
import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'

@Module({
  providers: [
    { provide: APP_FILTER, useClass: HttpExceptionFilter },
    { provide: APP_INTERCEPTOR, useClass: ApiInterceptor }
  ]
})
export class ApplicationModule {}

使用方法:

// user.controller.ts
@Get(':id')
async getUser(@Res() res, @Param('id', new ParseIntPipe()) id): Promise<User> {
  if(isNaN(id)) {
    throw new ApiException('ID不合法', ApiCode.PARAMS_ERROR, 200)
  }

  return { name: 'a', age: 18 }
}

HTTP状态码返回200还是4xx?

方案一:200 + 业务码 + message

方案二(RESTful):原生http status code + message