Skip to content
Express.js ex validation 4 min read

Validating with express-validator

express-validator is a set of Express middleware that wraps the battle-tested validator.js library. Instead of hand-writing if checks at the top of every route, you declare a chain of rules per field — each chain is itself a middleware that runs before your handler. It validates and sanitizes in the same pass, collects every failure into a structured result, and integrates cleanly with the Router. Because each validator is just middleware, you compose, share, and reuse rules the same way you do any other Express building block.

Installing and the chain model

Install the package alongside Express. It ships with TypeScript types built in.

npm install express-validator

The core idea is the validation chain. A function like body('email') returns a chain object that is also a middleware. You call methods on it (.isEmail(), .trim(), .notEmpty()) to append validators and sanitizers, and the order matters — sanitizers transform the value before the next validator sees it. The chain stores its findings on the request, and a separate step reads them back.

const { body, validationResult } = require('express-validator');

app.post(
  '/users',
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 8 }),
  (req, res) => {
    const result = validationResult(req);
    if (!result.isEmpty()) {
      return res.status(400).json({ errors: result.array() });
    }
    res.status(201).json({ email: req.body.email });
  }
);

Choosing the location: body, param, query

Each request location has its own factory function. They share the same chain API; only the source of the value differs.

FunctionReads fromTypical use
body('field')req.bodyJSON / form payloads
param('id')req.paramsRoute parameters like /users/:id
query('field')req.queryQuery string ?page=2
header('name')req.headersCustom headers
cookie('name')req.cookiesSigned/unsigned cookies
check('field')all of the aboveWhen you don’t care about location

A route that validates a path parameter, a query value, and a body field looks like this:

const { body, param, query, validationResult } = require('express-validator');

app.patch(
  '/posts/:id',
  param('id').isInt({ min: 1 }).toInt(),
  query('notify').optional().isBoolean().toBoolean(),
  body('title').trim().notEmpty().isLength({ max: 120 }),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
    res.json({ id: req.params.id, title: req.body.title, notify: req.query.notify });
  }
);

Tip: Sanitizers like .toInt() and .toBoolean() mutate the parsed value in place. After the chain runs, req.params.id is a real number, not the original string — so downstream code and your database driver receive correct types.

Chaining sanitizers and validators

Validators assert a condition (.isEmail(), .isLength()); sanitizers reshape the value (.trim(), .escape(), .toLowerCase()). Place sanitizers before the validators that depend on them. .optional() short-circuits the chain when the field is absent, and .withMessage() attaches a human-readable error to the validator immediately before it.

body('username')
  .trim()                              // sanitize first
  .toLowerCase()
  .notEmpty().withMessage('Username is required')
  .isLength({ min: 3, max: 20 }).withMessage('Must be 3-20 characters')
  .matches(/^[a-z0-9_]+$/).withMessage('Only lowercase letters, numbers, _');

For rules the library doesn’t ship, .custom() accepts a predicate that returns truthy/falsy or throws, and can be async for database lookups.

body('email')
  .isEmail().normalizeEmail()
  .custom(async (value) => {
    const existing = await db.findUserByEmail(value);
    if (existing) throw new Error('Email already in use');
    return true;
  });

Reading validationResult

validationResult(req) returns a Result object that aggregates every failure across all chains on the request. Its key methods:

  • isEmpty()true when nothing failed.
  • array() — a flat list of error objects.
  • mapped() — keyed by field, useful for form rendering.
  • throw() — throws if there are errors, handy with an async error handler.

A failing request produces structured output:

const result = validationResult(req);
console.log(result.array());

Output:

[
  { type: 'field', value: 'bob', msg: 'Must be 3-20 characters', path: 'username', location: 'body' },
  { type: 'field', value: 'no', msg: 'Invalid value', path: 'email', location: 'body' }
]

A reusable validation-handler middleware

Repeating the validationResult check in every handler is noise. Extract it into one middleware placed after the chains. It inspects the accumulated result and either short-circuits with a 422 or calls next() to reach the real handler.

// validate.js
const { validationResult } = require('express-validator');

const validate = (req, res, next) => {
  const errors = validationResult(req);
  if (errors.isEmpty()) return next();
  res.status(422).json({
    errors: errors.array().map((e) => ({ field: e.path, message: e.msg })),
  });
};

module.exports = validate;

Group the rules for a route into an array, then append validate as the last middleware before your handler. This keeps rules declarative and reusable across routes.

const express = require('express');
const { body } = require('express-validator');
const validate = require('./validate');

const router = express.Router();

const createUserRules = [
  body('email').trim().isEmail().normalizeEmail(),
  body('password').isLength({ min: 8 }).withMessage('Password too short'),
  body('age').optional().isInt({ min: 13 }).toInt(),
];

router.post('/users', createUserRules, validate, async (req, res) => {
  const user = await db.createUser(req.body);
  res.status(201).json(user);
});

module.exports = router;

Warning: The validation chain only writes to the request when its middleware actually runs. If you forget to register the chains (or place validate before them), validationResult returns an empty result and every request passes — a silent security hole. Always order rules first, then validate.

Best Practices

  • Order each chain as sanitizers first, then validators, so checks see the cleaned value.
  • Centralize the result check in one validate middleware instead of repeating validationResult in handlers.
  • Define rules as named arrays (createUserRules) so they can be shared between create/update routes.
  • Always sanitize string inputs with .trim(), and coerce types with .toInt()/.toBoolean() so downstream code gets real types.
  • Use .optional() for fields that may be absent rather than making every field required.
  • Return 422 Unprocessable Entity (or 400) with a stable error shape so clients can render field-level messages reliably.
  • Reach for .custom() with async for uniqueness and cross-field rules instead of duplicating logic in the handler.
Last updated June 14, 2026
Was this helpful?