Skip to content
Express.js ex libraries 5 min read

express-validator

Never trust incoming request data — query strings, bodies, params, and headers all arrive as untyped strings shaped by whoever sent them. express-validator wraps the battle-tested validator.js library in an Express-friendly middleware API, letting you declare per-field rules as a chain of .isEmail(), .isLength(), and .trim() calls that run before your route handler. It both validates (rejecting bad input) and sanitizes (normalizing good input), then exposes the collected errors through validationResult. This page covers validation chains, sanitizers, reading results, and wiring up a reusable error-formatting middleware.

Installing and the validation chain

express-validator is a standalone package. Install it, then build a rule for each field using the check-family functions — body, query, param, header, or cookie — chained with the validators you want to enforce.

npm install express-validator

Each function returns a validation chain, which is itself an Express middleware. You pass an array of chains to a route; express-validator runs them, accumulates any failures onto the request, and hands control to the next middleware regardless of whether validation passed. Your handler then decides what to do with the result.

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

const app = express();
app.use(express.json());

app.post(
  "/users",
  // Each entry is a validation chain — an Express middleware.
  [
    body("email").isEmail().withMessage("A valid email is required"),
    body("password")
      .isLength({ min: 8 })
      .withMessage("Password must be at least 8 characters"),
    body("age").optional().isInt({ min: 18, max: 120 }),
  ],
  (req, res) => {
    const result = validationResult(req);
    if (!result.isEmpty()) {
      return res.status(400).json({ errors: result.array() });
    }
    res.status(201).json({ created: req.body.email });
  }
);

app.listen(3000, () => console.log("Listening on http://localhost:3000"));

Sending an invalid payload produces a structured error list. The .withMessage() call attaches a human-readable message to the chain that precedes it.

Output:

$ curl -X POST localhost:3000/users -H 'Content-Type: application/json' \
    -d '{"email":"not-an-email","password":"short"}'

{
  "errors": [
    { "type": "field", "value": "not-an-email", "msg": "A valid email is required", "path": "email", "location": "body" },
    { "type": "field", "value": "short", "msg": "Password must be at least 8 characters", "path": "password", "location": "body" }
  ]
}

.optional() short-circuits the rest of the chain when the field is absent. Use .optional({ values: "falsy" }) to also skip when the value is "", 0, false, or null — handy for optional form fields submitted as empty strings.

Sanitizers normalize good input

Validators reject bad data; sanitizers transform acceptable data into a canonical form. Because chains run left to right, ordering matters: .trim() before .isLength() measures the trimmed length, and .normalizeEmail() after .isEmail() lowercases and canonicalizes the address. Sanitizers mutate the value in place, so req.body.email is already cleaned by the time your handler reads it.

app.post(
  "/signup",
  [
    body("email").trim().isEmail().normalizeEmail(),
    body("name").trim().escape().isLength({ min: 1 }),
    body("website").optional().trim().isURL().toLowerCase(),
    body("age").toInt().isInt({ min: 0 }),
  ],
  (req, res) => {
    const result = validationResult(req);
    if (!result.isEmpty()) {
      return res.status(400).json({ errors: result.array() });
    }
    // req.body.email is lowercased; req.body.age is a real number.
    res.json(req.body);
  }
);

The table below lists the sanitizers and validators you will reach for most often.

MethodKindEffect
.trim()sanitizerStrips surrounding whitespace
.escape()sanitizerHTML-escapes <, >, &, ', "
.normalizeEmail()sanitizerCanonicalizes the email address
.toInt() / .toFloat()sanitizerConverts string to a number
.toBoolean()sanitizerConverts to a real boolean
.isEmail()validatorAsserts a valid email
.isLength({ min, max })validatorAsserts string length bounds
.isInt({ min, max })validatorAsserts an integer in range
.matches(/regex/)validatorAsserts a regular-expression match
.custom(fn)validatorRuns your own predicate (sync or async)

.custom() accepts an async function, which is ideal for checks that hit a database — for example, confirming an email is not already registered. Returning a rejected promise (or throwing) marks the field invalid.

const { body } = require("express-validator");

const uniqueEmail = body("email").custom(async (value) => {
  const existing = await db.users.findByEmail(value);
  if (existing) {
    throw new Error("Email is already in use");
  }
  return true;
});

A reusable error-handling middleware

Repeating the validationResult check in every handler is noise. Factor it into a single middleware that runs after your chains, formats the errors consistently, and ends the request early on failure. Place it at the end of the chain array so it only runs once the validators have populated the request.

const { validationResult } = require("express-validator");

// Generic guard reused by every validated route.
function validate(req, res, next) {
  const result = validationResult(req);
  if (result.isEmpty()) {
    return next();
  }
  // Group messages by field name for a tidy client response.
  const errors = result.array().reduce((acc, err) => {
    acc[err.path] = err.msg;
    return acc;
  }, {});
  return res.status(422).json({ errors });
}

module.exports = validate;

Now each route declares only its rules, and validate enforces them.

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

app.post(
  "/articles",
  body("title").trim().isLength({ min: 3, max: 120 }),
  body("body").trim().isLength({ min: 1 }),
  validate,
  async (req, res) => {
    const article = await db.articles.create(req.body);
    res.status(201).json(article);
  }
);

Output:

$ curl -X POST localhost:3000/articles -H 'Content-Type: application/json' -d '{"title":"Hi"}'

{
  "errors": {
    "title": "Invalid value",
    "body": "Invalid value"
  }
}

To read only validated data and ignore unexpected fields, call matchedData(req) inside the handler — it returns just the keys that had validation chains, which is safer than passing the whole req.body to your database layer.

express-validator works identically on Express 4.x and 5.x, with one caveat: under Express 5, route paths and async error handling changed. Always send validation chains through the array/argument form shown here rather than awaiting them manually, so rejected custom validators surface as field errors instead of unhandled rejections.

Best Practices

  • Order sanitizers before validators in a chain so length and format checks run against the cleaned value.
  • Centralize the validationResult check in one reusable middleware instead of repeating it in every handler.
  • Use matchedData(req) to extract only validated fields, preventing mass-assignment of unexpected keys.
  • Attach a clear .withMessage() to each validator so clients receive actionable, field-specific errors.
  • Prefer async .custom() validators for database-backed rules (uniqueness, foreign-key existence) rather than checking inside the handler.
  • Return 422 Unprocessable Entity for validation failures to distinguish them from 400 malformed-request and 401/403 auth errors.
  • Keep reusable chains (like uniqueEmail) in a shared module and compose them across routes to avoid duplication.
Last updated June 14, 2026
Was this helpful?