首先完成“第4课:处理请求数据与构建 CRUD 端点”即可解锁本课程。转到上一课

引言

每个 API 都会接收到并非由其自身创建的数据。用户会发送格式错误的 JSON、缺失的字段、将字符串放在本应是数字的位置,以及技术上合法但逻辑上有误的请求体。如果没有验证层,这些问题会悄无声息地污染你的数据库,或触发晦涩难懂的 500 错误。如果没有统一的错误处理层,每个路由在出错时都会表现得五花八门——有的返回纯字符串,有的返回 HTML 错误页面,有的干脆什么都不返回。

本课将构建这两层:使用 express-validator 库进行请求验证,以及一个集中的错误处理器,让每个错误响应都具备相同的可预测结构。

验证传入的请求数据

express-validator 封装了经过充分测试的 validator.js 库,并可直接作为 Express 中间件接入。在你的项目中安装它:

bash
npm install express-validator

验证规则被定义为一个中间件数组,并附加到路由上,在 handler 执行前运行。下面是一个 POST /users 路由,要求传入合法的 email 和至少八个字符的 password:

javascript
const { body, validationResult } = require('express-validator');
const createUserRules = [
body('email').isEmail().withMessage('Must be a valid email address'),
body('password')
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters'),
];
router.post('/users', createUserRules, (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
// safe to use req.body here
res.status(201).json({ message: 'User created' });
});

调用 validationResult(req) 会收集整个规则链发现的所有违规项。如果该数组不为空,请求会被立即拒绝,并返回 HTTP 422 Unprocessable Entity——当服务器理解请求但因数据无效而无法处理时,这是语义上正确的状态码。

流程图,展示请求流转:传入的 POST 请求 -> 验证中间件链 -> validationResult 检查 -> 若有错误则返回 422 响应,若无错误则继续执行路由 handler

随着路由数量的增长,你可以将验证规则提取到单独的文件中,让路由文件只专注于业务逻辑。

构建集中的错误处理器

Express 有一个特殊的错误处理中间件签名:四个参数而不是三个。任何调用 next(err) 的中间件或路由,或在 async 包装器内抛出异常的,都会跳过普通中间件,最终到达这里。

为其创建一个独立文件:

javascript
// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
res.status(statusCode).json({
error: {
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
},
});
}
module.exports = errorHandler;

在主 app 文件中、于所有路由之后注册它。顺序很重要——Express 仅在错误处理器最后定义时才会调用它:

javascript
const errorHandler = require('./middleware/errorHandler');
app.use('/api', router);
app.use(errorHandler); // must come after all routes

现在,任何路由都可以通过给 Error 对象附加 statusCode 并传递给 next 来上报问题:

javascript
router.get('/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
const err = new Error('User not found');
err.statusCode = 404;
return next(err);
}
res.json(user);
} catch (error) {
next(error); // unexpected errors get 500
}
});

无论是否为预期错误,现在都会流经同一个处理器,并返回相同的 JSON 结构。前端团队只需编写一个统一的错误解析函数,而不必猜测每个端点会返回什么。

创建自定义 AppError 类

手动给原始 Error 对象附加 statusCode 属性会变得重复繁琐。一个简单的自定义类可以让这一切更整洁:

javascript
// utils/AppError.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true; // distinguishes expected errors from bugs
}
}
module.exports = AppError;

isOperational 标志让你的错误处理器对主动抛出的错误与未预期的运行时崩溃采取不同的处理方式:

javascript
const AppError = require('../utils/AppError');
// in a route
if (!user) {
return next(new AppError('User not found', 404));
}

你可以在错误处理器中检查 isOperational,以决定是输出完整的堆栈跟踪、发送告警,还是仅按原样返回 message。

示意图,展示两条错误路径:操作型错误(isOperational 为 true)走安全的 JSON 响应路径,未预期的错误(isOperational 为 false)走日志记录/告警步骤,然后返回通用的 500 响应

常见错误

把错误处理器放在路由之前是最常见的错误——它永远不会触发,因为此时还没有错误被传递进来。务必将其注册在最后。

对所有客户端错误一律使用 HTTP 400 是不够精确的。语法格式错误用 400,缺少认证用 401,用户已认证但无权限用 403,资源不存在用 404,结构合法但内容不满足业务规则用 422。

在生产环境中泄露堆栈跟踪是真实的安全风险。上文错误处理器示例中的条件展开(process.env.NODE_ENV === 'development')确保堆栈仅在本地环境出现。

忘记用 try/catch 包装 async 路由 handler 会导致未处理的 Promise 异常完全绕过你的错误处理器。一个轻量的 async 包装工具可以在不增加样板代码的情况下,在所有路由中强制执行这一模式。

总结

使用 express-validator 进行验证可以把脏数据挡在门外,并向客户端返回结构化的反馈。具有统一 JSON 结构的集中式错误处理器,能让你的 API 对使用者而言更可预测,也更易于随规模增长进行维护。AppError 类将这两件事串联起来——为你提供一种清晰的方式,从代码库的任何位置发出预期的失败信号,并由同一个错误处理器决定如何响应。

课后练习

课程检查点

巩固所学知识,获取作业反馈。

0/1
已回答 · 0/1