express-validator and Joi

The two alternatives you will meet in existing Express 24,430 code take opposite approaches. Both were run against the same bad body, { title: " ", year: 2100, isbn: "978" }.

express-validator 6,233 (github.com/express-validator/express-validator (https://github.com/express-validator/express-validator 6,233 ), npm 2,036 i express-validator, 7.3.2) is not a schema library: it is middleware built on the validator package, so each rule is a chain attached to one field, and the chains are the route's middleware array.

A validation chain with express-validatorJavaScript
app.post('/api/books', [
  body('title').trim().notEmpty().withMessage('title is required')
    .bail().isLength({ max: 200 }),
  body('year').isInt({ min: 1450, max: 2026 }).withMessage('year out of range').toInt(),
  body('isbn').matches(/^\d{13}$/).withMessage('ISBN must be 13 digits')
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
  res.json({ data: matchedData(req) });          // only fields a validator claimed
});

Each entry of errors.array() is { type, value, msg, path, location }; the body above yields three. Chains run left to right and sanitizers mutate as they go: trim() is why the reported value for title is "" rather than two spaces, toInt() is why a valid "2015" comes back as 2015, and bail() stops a chain after its first failure.

Joi 21,172 (github.com/hapijs/joi (https://github.com/hapijs/joi 21,172 ), npm i joi, 18.2.9) is a schema library like Zod 44,027 , older, with a fluent builder and no TypeScript inference.

The same rules as a Joi schemaJavaScript
const schema = Joi.object({
  title: Joi.string().trim().min(1).max(200).required(),
  year: Joi.number().integer().min(1450).max(2026).required(),
  isbn: Joi.string().pattern(/^\d{13}$/, 'ISBN').required()
});
const { error, value } = schema.validate(req.body, { abortEarly: false, stripUnknown: true });

Each entry in error.details carries a dotted path, a message, and a type — here string.empty, number.max and string.pattern.name, the last reading "isbn" with value "978" fails to match the ISBN pattern. Branch on type: it is stable, the message is not. abortEarly: false is mandatory in an API, since Joi's default stops at the first error; stripUnknown gives you Zod's key-dropping behavior; convert makes .trim() a sanitizer.

The three validators compared (npm, week of 2026-09-05)
Library Version Deps Weekly downloads Best at
zod 4.6.5 0 209.2 M TypeScript inference
joi 18.2.9 7 19.7 M Conditional rules
express-validator 7.3.2 2 1.2 M Retrofitting a route

All three are MIT licensed. The commercial gateways that sell request validation (Kong 3,957 , Apigee 1 , AWS API Gateway 24 ) check requests against an OpenAPI document at the network edge, complementing a library rather than replacing one: a gateway cannot know that an ISBN must not already exist. Starting fresh, choose Zod — one object is validator, type and OpenAPI schema at once.