06 ExpressValidator 类与项目级扩展
1. 为什么平时很少直接使用它
常规项目直接导入这些函数已经足够:
import {
body,
checkSchema,
matchedData,
query,
validationResult,
} from "express-validator";
这些导出可以理解为默认 ExpressValidator 能力的便捷入口。
当项目需要大量统一的自定义 validator、sanitizer 和错误格式时,创建自己的 ExpressValidator 实例会更有价值。
2. 构造函数
new ExpressValidator(
customValidators,
customSanitizers,
options,
)
三个参数分别负责:
- 自定义 validators;
- 自定义 sanitizers;
- 默认错误 formatter。
3. 定义项目级实例
import {
ExpressValidator,
type ValidationError,
} from "express-validator";
interface AppValidationError {
code: string;
field?: string;
message: string;
}
export const appValidator = new ExpressValidator(
{
isPositiveId(value: unknown): boolean {
const id = Number(value);
return Number.isInteger(id) && id > 0;
},
isSafeDisplayName(value: unknown): boolean {
if (typeof value !== "string") {
return false;
}
return !/[<>]/.test(value);
},
},
{
normalizeKeyword(value: unknown): string {
return String(value).trim().toLowerCase();
},
},
{
errorFormatter(error: ValidationError): AppValidationError {
if (error.type === "field") {
return {
code: "INVALID_FIELD",
field: error.path,
message: String(error.msg),
};
}
return {
code: "INVALID_REQUEST",
message: String(error.msg),
};
},
},
);
4. 自定义链方法
实例创建后,TypeScript 能识别扩展方法:
export const getWishValidation = [
appValidator
.param("id")
.isPositiveId()
.withMessage("id 必须是正整数")
.toInt(),
];
自定义 sanitizer:
export const searchValidation = [
appValidator
.query("keyword")
.optional()
.isString()
.normalizeKeyword()
.isLength({ max: 50 }),
];
扩展方法的名称应表达可复用、稳定的验证概念,例如:
isPositiveId
isSupportedLocale
isSafeFilename
normalizeKeyword
normalizePhoneNumber
不要创建含糊的 .isValid()。
5. 实例提供的 API
check
body
cookie
header
param
query
buildCheckFunction
checkExact
checkSchema
matchedData
oneOf
validationResult
因此可以让同一个实例贯穿规则定义、错误格式化和安全数据提取。
6. 实例级 validationResult()
因为构造函数配置了 formatter:
const errors = appValidator.validationResult(req);
这里得到的错误已经是 AppValidationError:
export function validateRequest(
req: Request,
res: Response,
next: NextFunction,
): void {
const errors = appValidator.validationResult(req);
if (!errors.isEmpty()) {
res.status(400).json({
code: "VALIDATION_ERROR",
errors: errors.array(),
});
return;
}
next();
}
7. 实例级 checkSchema()
自定义 validator 也能在 Schema 中使用:
export const wishIdSchema = appValidator.checkSchema({
id: {
in: ["params"],
isPositiveId: {
errorMessage: "id 必须是正整数",
},
toInt: true,
},
});
普通的顶层 checkSchema() 不知道该实例新增的扩展方法;要使用实例自己的 .checkSchema()。
8. CustomValidationChain 与 CustomSchema
需要给辅助函数标注扩展链类型时:
import type {
CustomSchema,
CustomValidationChain,
} from "express-validator";
type AppValidationChain = CustomValidationChain<
typeof appValidator
>;
type AppSchema = CustomSchema<typeof appValidator>;
示例:
function positiveWishId(): AppValidationChain {
return appValidator
.param("id")
.isPositiveId()
.toInt();
}
9. buildCheckFunction()
可以创建只检查指定位置的字段选择器:
const bodyOrQuery = appValidator.buildCheckFunction([
"body",
"query",
]);
bodyOrQuery("keyword").isString();
日常接口应优先使用明确的 .body() 或 .query();只有协议确实允许多个位置时才自定义选择器。
10. 异步自定义方法
export const appValidatorWithDatabase = new ExpressValidator({
async isUnusedWishName(value: unknown): Promise<boolean> {
if (typeof value !== "string") {
return false;
}
const wish = await Wish.findOne({
where: { name: value },
});
return wish === null;
},
});
使用:
appValidatorWithDatabase
.body("name")
.isString()
.bail()
.isUnusedWishName()
.withMessage("愿望名称已经存在");
不要让通用 validator 实例反向依赖大量业务 Model,否则基础校验模块会变成难以测试的业务中心。数据库特定扩展可以按领域拆分实例,或者继续使用局部 .custom()。
11. 什么时候值得使用
适合:
- 多个模块反复使用同一种业务格式;
- 希望所有错误统一格式化;
- 需要自定义链方法的 TypeScript 提示;
- Schema 也要使用自定义方法;
- 团队已经有稳定的校验规范。
不适合:
- 只有一两个自定义规则;
- 初学阶段尚未理解 ValidationChain;
- 扩展名称只被一个接口使用;
- 为了减少几行代码而建立复杂基础设施。
当前学习项目建议先掌握普通 ValidationChain,再引入 ExpressValidator。
12. 练习题
- 创建
isPositiveId和normalizeKeyword扩展。 - 配置实例级错误 formatter,并在中间件中使用实例的
validationResult()。 - 使用实例级 checkSchema 调用自定义 validator。
- 定义
CustomValidationChain<typeof appValidator>类型别名。 - 分析一个数据库查重规则应该放在全局实例还是局部 custom 中。
- 使用
buildCheckFunction()创建 body/query 选择器,并说明它的潜在歧义。