NodeJSexpress-validator

express-validator

express-validator takes a different shape from Joi and Zod: instead of a standalone schema you validate manually, it's a set of middleware that plug directly into your route's handler chain and operate on the Express request. It wraps the battle-tested validator.js library and integrates tightly with req.body/req.params/req.query. This page covers validation chains, collecting results, sanitization, and custom validators.

Setup and the validation chain

Bash
npm install express-validator

JS
import { body, validationResult } from 'express-validator'

app.post(
  '/users',
  // Each field is a chain of middleware — runs as part of the route:
  body('email').isEmail().withMessage('valid email required'),
  body('name').trim().isLength({ min: 2, max: 100 }),
  body('age').optional().isInt({ min: 0, max: 150 }).toInt(),
  body('role').default('user').isIn(['user', 'admin']),

  // Final handler reads the collected results:
  (req, res) => {
    const errors = validationResult(req)
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() })
    }
    createUser(req.body)
  },
)
Validators ARE middleware — listed inline in the route
Unlike schema libraries, express-validator's `body('email').isEmail()` *is* a [middleware](/nodejs/custom-middleware). You list one chain per field directly in the route's argument list; they run in order, accumulate errors onto the request, and your handler retrieves them with `validationResult(req)`. There's no separate schema object — the validation lives in the route definition.
Targeting different request locations

Function

Validates

body(field)

req.body

param(field)

req.params (route params)

query(field)

req.query (query string)

header(field)

Request headers

cookie(field)

req.cookies

check(field)

All of the above (any location)

JS
import { param, query } from 'express-validator'

app.get(
  '/users/:id',
  param('id').isInt().toInt(),                        // coerce route param
  query('include').optional().isIn(['orders', 'roles']),
  handler,
)
Validation AND sanitization in one chain

JS
body('email')
  .trim()              // sanitizer: strip surrounding whitespace
  .normalizeEmail()    // sanitizer: canonical form (lowercases, etc.)
  .isEmail()           // validator: must be a valid email

body('bio')
  .trim()
  .escape()            // sanitizer: HTML-escape to neutralize markup
  .isLength({ max: 500 })

body('age')
  .toInt()             // sanitizer: "42" → 42 (mutates req.body)
Sanitizers MUTATE `req.body` — order in the chain matters
Methods like `.trim()`, `.toInt()`, `.normalizeEmail()`, and `.escape()` are **sanitizers**: they modify the value in `req.body` in place. Because the chain runs left-to-right, ordering matters — `.trim()` before `.isLength()` so trailing spaces don't count toward length. After the chain, your handler reads the cleaned values directly from `req.body`. Validators check; sanitizers transform — and here they're freely mixed in one fluent chain.
Collecting and formatting errors

JS
const result = validationResult(req)

if (!result.isEmpty()) {
  return res.status(422).json({
    error: 'Validation failed',
    // Custom shape per error:
    details: result.array().map((e) => ({ field: e.path, message: e.msg })),
  })
}
// result.array() default shape:
[
  { type: 'field', path: 'email', msg: 'valid email required', location: 'body', value: 'nope' },
  { type: 'field', path: 'name',  msg: 'Invalid value',        location: 'body' }
]
Add `.withMessage()` or every error reads 'Invalid value'
Without `.withMessage('...')` on a validator, the default error message is the generic `"Invalid value"` — useless to clients. Attach a clear message to each validator (`.isEmail().withMessage('valid email required')`). `validationResult(req).array()` returns all collected errors; map them into your standard [error envelope](/nodejs/api-error-responses).
A reusable runner middleware

JS
// DRY: one middleware that runs after the chains and short-circuits on errors.
const handleValidation = (req, res, next) => {
  const result = validationResult(req)
  if (!result.isEmpty()) {
    return res.status(422).json({
      error: 'Validation failed',
      details: result.array().map((e) => ({ field: e.path, message: e.msg })),
    })
  }
  next()
}

app.post('/users',
  body('email').isEmail().withMessage('valid email required'),
  body('name').trim().notEmpty(),
  handleValidation,        // checks results once, here
  createUserHandler,       // runs only if validation passed
)
Factor the result-check into shared middleware
Rather than repeat the `validationResult` boilerplate in every handler, put it in one `handleValidation` middleware placed after the field chains. The handler then runs only on valid input — keeping it clean, the same goal as the [validate() factory](/nodejs/zod) pattern used with schema libraries.
Custom validators

JS
body('email').custom(async (value) => {
  const existing = await db.users.findByEmail(value)
  if (existing) throw new Error('Email already in use')   // → becomes the msg
  return true
})

body('confirm').custom((value, { req }) => {
  if (value !== req.body.password) throw new Error('Passwords must match')
  return true
})
`.custom()` handles async checks and cross-field rules
A custom validator returns `true` (or resolves) to pass, or throws/rejects to fail (the thrown message becomes the error). This is where express-validator shines for **async** checks against a database — like uniqueness ("email already taken") — which schema libraries can't do as naturally. The `{ req }` argument enables cross-field rules (password confirmation).
Choosing among the three

Choose…

When

You want validation co-located in routes & tight Express integration, plus easy async/DB checks

You're on TypeScript and want inferred types from one schema

You want a rich, framework-agnostic schema for any JS value

All three are good — match the tool to the project
There's no single winner: express-validator feels native to Express and excels at request-bound, async validation; Zod wins for TypeScript type-safety; Joi offers the broadest standalone rule set. Pick the one whose ergonomics fit your stack and stick with it consistently across the codebase — mixing validators on different routes is the real mistake.
Next
From guarding inputs to persisting data — the databases section: [Databases with Node.js](/nodejs/databases-intro).