06 ExpressValidator 类与项目级扩展

1. 为什么平时很少直接使用它

常规项目直接导入这些函数已经足够:

import {
  body,
  checkSchema,
  matchedData,
  query,
  validationResult,
} from "express-validator";

这些导出可以理解为默认 ExpressValidator 能力的便捷入口。

当项目需要大量统一的自定义 validator、sanitizer 和错误格式时,创建自己的 ExpressValidator 实例会更有价值。

2. 构造函数

new ExpressValidator(
  customValidators,
  customSanitizers,
  options,
)

三个参数分别负责:

  1. 自定义 validators;
  2. 自定义 sanitizers;
  3. 默认错误 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. CustomValidationChainCustomSchema

需要给辅助函数标注扩展链类型时:

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. 什么时候值得使用

适合:

不适合:

当前学习项目建议先掌握普通 ValidationChain,再引入 ExpressValidator。

12. 练习题

  1. 创建 isPositiveIdnormalizeKeyword 扩展。
  2. 配置实例级错误 formatter,并在中间件中使用实例的 validationResult()
  3. 使用实例级 checkSchema 调用自定义 validator。
  4. 定义 CustomValidationChain<typeof appValidator> 类型别名。
  5. 分析一个数据库查重规则应该放在全局实例还是局部 custom 中。
  6. 使用 buildCheckFunction() 创建 body/query 选择器,并说明它的潜在歧义。

官方参考