优雅处理状态码:Nest.js 实现的封装方案
本文将介绍如何优雅地处理 Nest.js 项目中的 API 状态码。在 Nest.js 项目中,我们经常需要抛出异常或返回错误信息,这就需要处理合适的状态码,以便客户端够正确地处理返回结果。
项目中的状态码定义的太松散或太严格,都不太好。过于松散,随着项目的逐渐开发,会导致混乱,而过于严格(比如每一个业务都有一个确定的状态码),快速迭代开发的时候又有点束缚。所以这里我先预定义一套简单的规则,0
代表请求成功,1
代表请求异常。然后再根据业务去定义具体的状态码,两者可以互相结合使用。
首先,定义一个继承自 Nest.js 框架 HttpException
类的 ApiException
类。后续我们就通过使用这个自定义实现,来抛出 API 消息。
// api.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
interface ApiExceptionOptions {
/** API抛出的状态码 */
code: number;
/** API抛出的错误消息 */
message: string;
}
type ApiError = string | ApiExceptionOptions;
/** API异常 */
export class ApiException extends HttpException {
/**
*
* @param errorOrObj 错误消息,或者是表示错误详情的对象
* @param status HTTP状态码,默认 200
*/
constructor(errorOrObj: ApiError, status: HttpStatus = HttpStatus.OK) {
// 因为还需要在异常过滤器(exception.filter.ts)中统一格式化
// 所以抛出的异常需要是 {code, message}, httpStatus 这种格式
if (typeof errorOrObj === 'string') {
super({ code: 1, message: errorOrObj }, status);
} else {
super({ code: errorOrObj.code, message: errorOrObj.message }, status);
}
}
}
上述代码中,我们定义了一个 ApiExceptionOptions
接口,表示 API 的错误详情,它包含一个状态码和一个错误消息。
ApiError
类型是首个参数的类型,可以是一个字符串错误消息,也可以是一个 ApiExceptionOptions
对象。通过判断 errorOrObj
的类型,视情况调用 super
方法来配置状态码和错误消息。第二个参数是 HTTP 的状态码,默认是 200 。
这样,当我们创建了一个 ApiException
实例时,就可以通过 message
属性来访问错误消息,而状态码则存储在 response
属性中。
在实际的应用中,就可以使用这个自定义的 ApiException
类来快速的创建错误响应并返回给客户端。
上面我们已经创建了一个自定义的 ApiException
类,来抛出 API 异常。下面我们还需要在 Nest.js 项目中统一处理这些异常,并返回正确的响应信息。
⚠️ 注意:主要是从性能方面考虑,我一般都是默认将 Nest.js 依赖的 Web 框架从 Express 更改为 Fastify。 所以下面的请求对象和响应对象都是 Fastify 的,如果你使用的是 Express,将下面 Fastify 的部分改为使用 Express 实现即可。
// exception.filter.ts
import {
ExceptionFilter,
Catch,
HttpException,
ArgumentsHost,
HttpStatus,
} from '@nestjs/common';
import { FastifyRequest, FastifyReply } from 'fastify';
import { ApiException } from '../exceptions/api.exception';
/** 默认的错误代码 */
const DEFAULT_ERROR_CODE = 1;
/** 从异常里获取HTTP状态码 */
function getHttpStatus(exception: HttpException) {
if (exception instanceof HttpException) {
return exception.getStatus();
}
return HttpStatus.INTERNAL_SERVER_ERROR;
}
/** 获取API业务抛出的错误代码 */
function getApiCode(exception: HttpException): number {
// 如果是ApiException的实例,就可以获取自定义状态码
if (exception instanceof ApiException) {
return (exception as any)?.response?.code;
}
// 否则返回默认的错误状态码
return DEFAULT_ERROR_CODE;
}
/** 获取异常的错误消息 */
function getErrorMessage(exception: HttpException) {
return (
(exception as any)?.response?.message ||
(exception as any)?.message ||
'未知错误'
);
}
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest<FastifyRequest>();
const response = ctx.getResponse<FastifyReply>();
const httpStatus = getHttpStatus(exception);
const code = getApiCode(exception);
const msg = getErrorMessage(exception);
const data = { code, msg };
response.code(httpStatus).type('application/json').send(data);
}
}
上述代码中,我们定义了一个 HttpExceptionFilter
类,它实现了 ExceptionFilter
接口,用于拦截并处理应用程序中抛出的异常。在 catch
方法中,我们获取请求和响应对象,然后根据异常对象的类型,来生成相应的错误信息。
getHttpStatus
函数用于获取异常对象中的 HTTP 状态码。如果异常对象不是 HttpException
类型,就默认返回 500 状态码。
getApiCode
函数用于获取异常对象中自定义的错误码,如果异常对象是我们自定义的 ApiException
类型,则获取自定义状态码,否则返回一个默认的错误码,以方便进一步的处理。
getErrorMessage
函数用于获取异常对象中自定义的错误消息,如果异常对象是我们自定义的 ApiException
类型,则获取自定义消息,否则获取异常对象中的默认错误消息,如果不存在,则返回一个 “未知错误” 的消息。
获取到必要的消息之后,生成了一个包含错误状态码、错误消息的响应对象 data
,最后,我们将响应对象发送回客户端,并在响应头中指定返回的数据类型为 JSON 格式。
由于我们在 API 层面进行了统一封装,因此完全可以在 main.ts
中将该异常过滤器全局注册,这样就可以捕获应用程序运行时的任何异常。同时,由于在上面我们进行了格式化,使抛出的异常信息都具有了统一的格式,客户端可以更方便的处理这些异常信息。
// main.ts
import { HttpExceptionFilter } from './common/filters/exception.filter';
// ...
app.useGlobalFilters(new HttpExceptionFilter());
我们现在可以在应用中方便的统一处理状态码和异常信息了: 如:
// 抛出一个默认的API异常
// 错误码1;错误消息:鉴权未通过;HTTP状态码 200
throw new ApiException('鉴权未通过');
// 抛出一个自定义状态码的异常
// 错误码10010;错误消息:鉴权未通过;HTTP状态码 200
throw new ApiException({ code: 10010, message: '鉴权未通过' });
// 抛出一个自定义状态码和自定义HTTP状态码的异常
// 错误码10010;错误消息:鉴权未通过;HTTP状态码 401
throw new ApiException({ code: 10010, message: '鉴权未通过' }, HttpStatus.UNAUTHORIZED);
这是一种灵活而松散的使用方式。
除此之外,我们还可以针对各个模块的业务进行进一步的封装,使状态码更严谨。
例如,我们可以将 10000
系列作为系统层面的状态码,20000
系列作为具体业务模块的状态码。
举个例子:
// 假设这里我们用户模块的状态码为 21000 系列
// 那么我们可以这么封装
// user.exception.ts
import { ApiException } from './api.exception';
const enum ErrorCode {
/** 用户已存在 */
IS_EXIST = 21000,
/** 用户不存在 */
NOT_EXIST = 21001,
/** 用户已被封禁 */
IS_BAN = 21002,
}
/** 异常:用户已存在 */
export class UserIsExistException extends ApiException {
constructor() {
super({ code: ErrorCode.IS_EXIST, message: '用户已存在' });
}
}
/** 异常:用户不存在 */
export class UserNotExistException extends ApiException {
constructor() {
super({ code: ErrorCode.NOT_EXIST, message: '用户不存在' });
}
}
// 这样,我们在业务层面,可以直接拿来使用了
// user.controller.ts
if (!user) {
throw new UserNotExistException();
}
当然,具体还是要根据自己的业务来进行定义。这里只是一家之言,起个抛砖引玉的作用。
最后,通过以上这些处理,我们终于实现了一个优雅的处理异常的机制,可以根据应用程序抛出的具体异常类型,生成规范的错误信息并返回给客户端。