在 API 的边缘校验数据,并把所有错误处理集中到一个四参数的中间件里。
在 Kodokon 中打开本课每一份传入的数据在被证明清白之前都是敌意的:JSON 请求体、URL 参数、请求头。校验应放在应用的边缘,也就是控制器之前,好让内层能在有保障的数据上工作。接着要区分两类错误:运营型错误(无效输入、资源缺失、冲突),它们是预期之内的,会被翻译成 4xx;以及编程型错误(bug),它们必须在客户端一侧产生一个不透明的 500,在服务器一侧留下完整的堆栈跟踪。
export class HttpError extends Error {
constructor(status, message, details = undefined) {
super(message);
this.status = status;
this.details = details;
}
}一个校验中间件会在控制器之前拦截请求。下面这个手写版本展示了原理;在生产环境中,一个声明式的 schema(zod 已成为标准)还额外提供类型推断和结构化的消息。它的取舍是:多一个依赖和一点运行时开销,换来集中、可组合、不可能忘记的规则。
import { HttpError } from './http-error.js';
export function validateUser(req, res, next) {
const { email, name } = req.body ?? {};
const errors = [];
if (typeof email !== 'string' || !email.includes('@')) {
errors.push('email: invalid format');
}
if (typeof name !== 'string' || name.trim().length < 2) {
errors.push('name: 2 characters minimum');
}
if (errors.length > 0) {
return next(new HttpError(400, 'Invalid body', errors));
}
next();
}Express 通过一个中间件的四参数签名 (err, req, res, next) 来识别它是错误中间件。用 app.use(errorHandler) 注册在所有路由之后、位于最末尾,它就成了 API 唯一的错误出口:统一的响应格式、集中于一处的日志、不泄露任何内部信息。而各个控制器只需调用 next(err)。
export function errorHandler(err, req, res, next) {
if (res.headersSent) {
return next(err);
}
const status = err.status ?? 500;
const body = { error: err.message };
if (err.details) {
body.details = err.details;
}
if (status >= 500) {
console.error(err);
body.error = 'Internal server error';
}
res.status(status).json(body);
}