4.2 统一响应格式设计
成功响应
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| status | number | 是 | 状态码,0 表示成功 |
| data | any | 是 | 响应数据 |
| msg | string | 否 | 提示信息(一般成功不需要) |
| count | number | 否 | 列表总数(列表接口用) |
| offset | number | 否 | 分页偏移(列表接口用) |
| size | number | 否 | 每页大小(列表接口用) |
错误响应
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| status | number | 是 | 错误码(非 0) |
| msg | string | 是 | 错误信息 |
| data | any | 否 | 额外的错误数据 |
状态码约定
| 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.getById 里 throw new AppError(...),Express 5 会自动捕获并交给 errorHandler。
4.8 小结
- 统一响应格式:成功返回
{ status: 0, data },错误返回{ status: 非0, msg } - 用
AppError类抛出业务错误,Service 层不关心 HTTP 响应 - 全局错误处理中间件统一捕获所有错误
- 错误处理中间件必须是 4 个参数
- 404 中间件放在路由之后、错误处理之前