Express中间件的标准签名是function middlewareName(req, res, next)。验证中间件在这个函数中读取req.body等请求数据,检查字段是否存在、类型和格式是否符合要求。验证失败时,用res.status(400).json(...)返回错误,并且不要调用next;验证通过时,调用next()把控制权交给后续处理器。原文强调两个关键点:验证通过却忘记调用next,请求会挂起,客户端收不到响应;验证失败后仍调用next,后续业务逻辑可能继续执行。因此验证中间件应放在业务处理器之前,并通常在express.json()之后,以便访问解析后的req.body。
一、手写验证中间件
以用户注册接口为例,需要确保username和email存在且格式正确。可以创建独立模块封装验证逻辑,代码来自原文示例:
- // middleware/validateUser.js
- const validateUser = (req, res, next) => {
- const { username, email } = req.body;
- const errors = [];
- // 验证用户名
- if (!username || typeof username !== 'string' || username.trim().length < 3) {
- errors.push('Username is required and must be at least 3 characters long');
- }
- // 验证邮箱格式
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- if (!email || !emailRegex.test(email)) {
- errors.push('Valid email address is required');
- }
- // 如果有错误,返回 400 状态码并停止后续执行
- if (errors.length > 0) {
- return res.status(400).json({
- status: 'error',
- message: 'Validation failed',
- details: errors
- });
- }
- // 验证通过,继续执行下一个中间件或路由处理器
- next();
- };
- module.exports = validateUser;
复制代码
该中间件先用errors数组收集多个字段的错误,再统一返回400响应。username要求为字符串且trim后长度不少于3,email用正则测试。只有errors为空时才调用next。
挂载到路由时,中间件顺序很关键:先解析JSON请求体,再执行验证中间件,最后进入业务处理器。示例:
- const express = require('express');
- const app = express();
- const validateUser = require('./middleware/validateUser');
- // 解析 JSON 请求体
- app.use(express.json());
- // POST /register 路由
- // 顺序:1. parse JSON -> 2. validateUser -> 3. handler
- app.post('/register', validateUser, (req, res) => {
- // 此时可以确信 req.body.username 和 req.body.email 是有效的
- const { username, email } = req.body;
- // 模拟数据库操作
- console.log(`Creating user: ${username}, ${email}`);
- res.status(201).json({
- status: 'success',
- message: 'User registered successfully',
- data: { username, email }
- });
- });
- app.listen(3000, () => console.log('Server running on port 3000'));
复制代码
这样写适合字段较少、规则简单的场景。优点是不依赖额外库,缺点是复杂项目里容易堆积正则和条件判断,维护成本会上升。
二、express-validator链式验证
原文推荐在复杂项目中使用express-validator。它基于validator.js,提供链式调用验证规则和数据净化功能。安装命令:
- npm install express-validator
复制代码
注册接口可以定义验证规则数组,并用validationResult(req)读取结果。示例代码:
- const { body, validationResult } = require('express-validator');
- const express = require('express');
- const app = express();
- app.use(express.json());
- // 定义验证规则数组
- const registerValidationRules = [
- body('username')
- .trim()
- .notEmpty().withMessage('Username is required')
- .isLength({ min: 3 }).withMessage('Username must be at least 3 characters'),
- body('email')
- .normalizeEmail()
- .isEmail().withMessage('Invalid email format'),
- body('password')
- .isLength({ min: 8 }).withMessage('Password must be at least 8 characters')
- .matches(/[0-9]/).withMessage('Password must contain a number')
- ];
- // 通用错误处理中间件(可选,用于统一处理验证结果)
- const handleValidationErrors = (req, res, next) => {
- const errors = validationResult(req);
- if (!errors.isEmpty()) {
- return res.status(400).json({ errors: errors.array() });
- }
- next();
- };
- // 应用验证规则
- app.post('/register',
- registerValidationRules, // 1. 执行验证规则
- handleValidationErrors, // 2. 检查验证结果
- (req, res) => {
- // 3. 业务逻辑
- res.json({ message: 'User created', data: req.body });
- }
- );
- app.listen(3000);
复制代码
这里的body('username')、body('email')、body('password')分别针对字段建立规则。trim()和normalizeEmail()属于原文提到的数据净化能力,notEmpty()、isLength()、isEmail()、matches()负责格式和完整性校验。handleValidationErrors把validationResult(req)的结果统一转成400响应,避免在每个路由里重复写错误检查。
三、可复用的验证工厂
如果多个路由都要执行“运行规则、检查结果、失败返回400”的流程,可以用工厂函数生成中间件。原文示例:
- const { body, validationResult } = require('express-validator');
- // 创建一个通用的验证中间件生成器
- const createValidator = (validations) => {
- return async (req, res, next) => {
- // 执行所有传入的验证规则
- await Promise.all(validations.map(validation => validation.run(req)));
- const errors = validationResult(req);
- if (errors.isEmpty()) {
- return next();
- }
- res.status(400).json({ errors: errors.array() });
- };
- };
- // 定义具体的验证规则
- const userRegistrationValidations = [
- body('email').isEmail(),
- body('password').isLength({ min: 6 })
- ];
- // 生成中间件
- const validateRegistration = createValidator(userRegistrationValidations);
- // 使用
- app.post('/signup', validateRegistration, (req, res) => {
- res.send('Signup successful');
- });
复制代码
工厂函数的参数是验证规则数组,返回一个async中间件。它用Promise.all(validations.map(validation => validation.run(req)))执行所有规则,再通过validationResult(req)统一判断。验证通过调用next,失败返回400和errors.array()。这种方式把规则定义与执行流程分离,适合多个接口复用同一套验证逻辑。
四、最佳实践与适用场景
1. 分离关注点:验证中间件只负责数据格式和完整性检查,不应包含数据库查询或业务逻辑。原文指出,像“用户是否已存在”属于业务逻辑,而“邮箱格式是否正确”属于验证。
2. 统一错误格式:所有验证失败响应应保持相同的JSON结构,便于前端统一处理。手写示例返回status、message、details,express-validator示例返回errors数组,项目中应统一选择一种格式。
3. 数据净化:验证的同时可进行trim()去除空格、normalizeEmail()规范化邮箱,必要时使用escape()防止XSS。express-validator内置了这些能力。
4. 异步支持:如果验证过程需要异步操作,中间件函数应声明为async,并用await等待异步操作完成。原文也提醒,验证层应聚焦格式与完整性,像用户名唯一性这类业务判断不宜随意混入验证中间件。
5. 位置安排:验证中间件必须位于express.json()之后,否则无法访问解析后的req.body;同时要放在业务处理器之前,确保无效请求不会进入后续逻辑。
从原文代码可以推导,手写中间件适合规则简单的接口,express-validator链式规则适合规则较多、需要净化和统一错误处理的项目,验证工厂则适合多路由复用。无论采用哪种方式,核心都是控制next()的调用时机、保持错误响应一致,并让验证层与业务层职责清晰。 |