04 — 统一响应与错误处理

4 阅读4分钟

4.2 统一响应格式设计

成功响应

字段类型必填说明
statusnumber状态码,0 表示成功
dataany响应数据
msgstring提示信息(一般成功不需要)
countnumber列表总数(列表接口用)
offsetnumber分页偏移(列表接口用)
sizenumber每页大小(列表接口用)

错误响应

字段类型必填说明
statusnumber错误码(非 0)
msgstring错误信息
dataany额外的错误数据

状态码约定

status含义
0成功
1通用错误
2参数错误 / 业务校验失败
3无权限
4未登录
404资源不存在
500服务器内部错误

你可以根据自己的业务扩展。


4.3 响应工具函数

src/utils/response.ts

import type { Response } from 'express';

export interface ApiResponse<T = unknown> {
  status: number;
  msg?: string;
  data?: T;
  count?: number;
  offset?: number;
  size?: number;
}

type SuccessMeta = Omit<ApiResponse<never>, 'status' | 'data'>;

interface ErrorOptions {
  status: number;
  msg: string;
  httpStatus?: number;  // HTTP 状态码,默认 200
  data?: unknown;
}

// 成功响应
export function sendOk<T>(response: Response, data: T, meta: SuccessMeta = {}) {
  return response.json({
    status: 0,
    ...meta,
    data,
  } satisfies ApiResponse<T>);
}

// 列表响应(自动带 count)
export function sendList<T>(response: Response, data: readonly T[], meta: SuccessMeta = {}) {
  const { count = data.length, ...rest } = meta;
  return sendOk(response, data, { ...rest, count });
}

// 错误响应
export function sendError(
  response: Response,
  { status, msg, httpStatus = 200, data }: ErrorOptions,
) {
  const body: ApiResponse = {
    status,
    msg,
    ...(data === undefined ? {} : { data }),
  };
  return response.status(httpStatus).json(body);
}

使用示例

// 成功返回单个对象
sendOk(res, { id: 1, name: '项目A' });

// 成功返回列表
sendList(res, items);

// 带分页信息的列表
sendList(res, items.slice(offset, offset + size), {
  count: items.length,
  offset,
  size,
});

// 错误
sendError(res, { status: 2, msg: '参数错误' });

4.4 自定义错误类

为什么需要自定义错误类?因为业务错误(比如"名称已存在")和程序错误(比如 undefined 报错)应该区分处理。

src/errors/app-error.ts

export class AppError extends Error {
  readonly status: number;

  constructor(message: string, options?: { status?: number }) {
    super(message);
    this.name = 'AppError';
    this.status = options?.status ?? 1;
  }
}

在 Service 里抛业务错误

import { AppError } from '@/errors/app-error.js';

function createProject(name: string) {
  if (name === '') {
    throw new AppError('项目名称不能为空', { status: 2 });
  }
  if (exists(name)) {
    throw new AppError('项目名称已存在', { status: 2 });
  }
  // ...
}

Service 层只需要 throw,不需要关心怎么返回给前端。错误处理交给全局中间件。


4.5 全局错误处理中间件

src/middlewares/error-handler.ts

import type { ErrorRequestHandler } from 'express';
import { AppError } from '@/errors/app-error.js';
import { sendError } from '@/utils/response.js';

export const errorHandler: ErrorRequestHandler = (error, _request, response, _next) => {
  // 业务错误:正常返回,HTTP 状态码用 200
  if (error instanceof AppError) {
    return sendError(response, {
      status: error.status,
      msg: error.message,
    });
  }

  // 未知错误:打日志,返回 500
  console.error('[Server Error]', error);

  return sendError(response, {
    status: 500,
    msg: '服务器内部错误',
    httpStatus: 500,
  });
};

错误处理中间件的签名

必须是4 个参数(error, req, res, next),Express 才会把它当错误处理中间件。

少一个参数都不行。比如你写 (error, req, res),Express 会以为它是普通中间件。

为什么业务错误的 HTTP 状态码是 200

这是一种设计选择,不是标准答案。两种做法:

做法优点缺点
HTTP 200 + body 里的 status前端统一处理,不用判断 HTTP 状态不符合 REST 规范
HTTP 状态码表示错误类型符合 REST 规范,CDN/网关能识别前端要处理多种 HTTP 错误

管理后台项目里,用「HTTP 200 + 业务状态码」的方式更省心,前端只用写一套错误处理逻辑。


4.6 404 中间件

src/middlewares/not-found.ts

import type { RequestHandler } from 'express';
import { sendError } from '@/utils/response.js';

export const notFound: RequestHandler = (_request, response) => {
  return sendError(response, {
    status: 404,
    msg: '接口不存在',
    httpStatus: 404,
  });
};

注意:404 中间件要放在所有路由之后,但要在错误处理中间件之前


4.7 异步错误怎么捕获

Express 5 会自动捕获 async 函数里抛出的错误,直接传给错误处理中间件。

如果你用的是 Express 4,async 函数里抛的错误不会被自动捕获,需要用 try/catch 包一下,或者用 express-async-errors 库。

Express 5 的写法(直接抛就行):

router.get('/project/:id', async (req, res) => {
  const project = await service.getById(Number(req.params.id));
  sendOk(res, project);
});

如果 service.getByIdthrow new AppError(...),Express 5 会自动捕获并交给 errorHandler


4.8 小结

  • 统一响应格式:成功返回 { status: 0, data },错误返回 { status: 非0, msg }
  • AppError 类抛出业务错误,Service 层不关心 HTTP 响应
  • 全局错误处理中间件统一捕获所有错误
  • 错误处理中间件必须是 4 个参数
  • 404 中间件放在路由之后、错误处理之前