DEV Community

Deval Ujeniya
Deval Ujeniya

Posted on

express-validator

express-validator

A practical set of notes covering validation chains, sanitization, custom validators, error handling, field selection, wildcards, manual validation, and schema validation.

1. Getting Started

express-validator is used with Express to validate and sanitize request data.

Basic setup

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

const app = express();

app.get('/hello',
  query('person').notEmpty(),
  (req, res) => {
    const result = validationResult(req);

    if (!result.isEmpty()) {
      return res.status(400).json({ errors: result.array() });
    }

    res.send(`Hello, ${req.query.person}`);
  }
);
Enter fullscreen mode Exit fullscreen mode

Important idea

Validation chains run against the request, but validators do not automatically send a response. You must inspect the result with validationResult(req) and decide how to respond.


2. Errors and matchedData()

After running validation, use validationResult(req) to retrieve validation errors.

const result = validationResult(req);

if (!result.isEmpty()) {
  return res.status(400).json({ errors: result.array() });
}
Enter fullscreen mode Exit fullscreen mode

A validation error can contain information such as the field, location, value, message, and path.

Sanitization

Sanitizers transform input before you use it. For example, escape() can be used to escape characters that could otherwise be involved in XSS.

query('search').escape()
Enter fullscreen mode Exit fullscreen mode

matchedData(req)

matchedData(req) retrieves the data that matched the validation/sanitization chains.

const data = matchedData(req);
Enter fullscreen mode Exit fullscreen mode

This is useful when you want to work with the validated and sanitized request data rather than using the entire raw request object.


3. Validation Chains

A validation chain is built from a field location followed by validators, sanitizers, and modifiers.

Common field locations include:

  • body()
  • param()
  • query()

Example:

body('email')
  .optional()
  .trim()
  .isEmail();
Enter fullscreen mode Exit fullscreen mode

Validators

Validators check whether the value satisfies a condition.

Examples from the notes:

.isEmail()
.isLength({ min: 6 })
.notEmpty()
Enter fullscreen mode Exit fullscreen mode

Sanitizers

Sanitizers transform the value.

Examples:

.trim()
.escape()
Enter fullscreen mode Exit fullscreen mode

Modifiers

Modifiers change how a validation chain behaves.

For example:

.optional()
Enter fullscreen mode Exit fullscreen mode

4. Types, Order, and Reusing Chains

Standard validators cast values to strings

Standard validators operate on values as strings. Keep this behavior in mind when validating request data.

Order matters

The order of validators and sanitizers can change the result.

For example:

trim().notEmpty()
Enter fullscreen mode Exit fullscreen mode

is different from:

notEmpty().trim()
Enter fullscreen mode Exit fullscreen mode

If whitespace needs to be removed before checking whether a value is empty, the order of the chain matters.

Validation chains are mutable

Chains are mutable. When you build reusable validation chains, be careful not to unintentionally modify a chain that is reused elsewhere.

Factory functions can be used when you need a fresh chain each time.


5. Field Selection and Paths

express-validator supports selecting fields using paths.

Examples include nested fields and array indexes.

For nested data, the path describes where the field exists inside the request data.

Arrays and nested fields

Wildcard selection can be used when the same validation needs to be applied to multiple array elements.

For example:

body('siblings.*.name').notEmpty()
Enter fullscreen mode Exit fullscreen mode

This selects the name field from every object inside siblings.

A path such as:

siblings.name
Enter fullscreen mode Exit fullscreen mode

does not select the name field inside every sibling object. Use the wildcard when the field exists inside array elements.

Special characters in field names

When a field name itself contains special characters such as a dot, the field path needs to represent the literal field name correctly.

The notes use bracket notation such as:

body('["a.b"]')
Enter fullscreen mode Exit fullscreen mode

for a literal field name containing a dot.


6. Wildcards and Globstars

Wildcards are useful when working with dynamic nested data.

* — wildcard

* matches one path segment.

Example:

body('siblings.*.name').notEmpty()
Enter fullscreen mode Exit fullscreen mode

** — globstar

** can match any depth in a nested structure.

The distinction is:

  • * → one segment
  • ** → any depth

This becomes useful when the structure can contain nested levels that are not fixed in advance.


7. Custom Validators and Sanitizers

When built-in validators are not enough, you can create custom validation logic with .custom().

Custom validator

body('email').custom(value => {
  // custom validation logic
  return true;
});
Enter fullscreen mode Exit fullscreen mode

A custom validator can also be asynchronous.

body('email').custom(async value => {
  // async validation logic
});
Enter fullscreen mode Exit fullscreen mode

If a custom validator throws an error or rejects, the validation fails.

Custom sanitizer

Custom sanitizers transform the value.

An important rule from the notes: a custom sanitizer must return the transformed value.

Example:

body('name').customSanitizer(value => {
  return value.trim();
});
Enter fullscreen mode Exit fullscreen mode

8. Error Messages

Validation errors can have custom messages using .withMessage().

body('email')
  .isEmail()
  .withMessage('Bad e-mail');
Enter fullscreen mode Exit fullscreen mode

Errors can also come from thrown errors inside custom validators.

Message precedence

The notes cover field-level fallback and message precedence. The important practical point is to define the message at the validation step where you want the specific error to be associated.

Example:

body('password')
  .isLength({ min: 6 })
  .withMessage('Password must be at least 6 characters');
Enter fullscreen mode Exit fullscreen mode

9. ExpressValidator Class

The ExpressValidator class can be used to create an instance containing custom validators and sanitizers.

This is useful when you want to organize reusable validation logic.

The notes cover adding custom validators and custom sanitizers through the class and then using the resulting validation helpers in the application.


10. Manually Running Validation

Validation chains do not have to be used only as Express route middleware.

They can also be run manually with .run(req).

await validationChain.run(req);
Enter fullscreen mode Exit fullscreen mode

This uses the ContextRunner behavior to execute the validation chain against the request.

Manual running is useful when validation needs to be triggered from application logic rather than directly placed in the middleware list.


11. Schema Validation with checkSchema()

For larger validation rules, checkSchema() can define validation using an object schema.

Example structure:

const { checkSchema } = require('express-validator');

const userSchema = {
  email: {
    trim: true,
    isEmail: true,
  },
  password: {
    isLength: {
      options: { min: 6 },
    },
  },
};

app.post('/signup',
  checkSchema(userSchema),
  (req, res) => {
    const result = validationResult(req);

    if (!result.isEmpty()) {
      return res.status(400).json({ errors: result.array() });
    }

    res.json({ message: 'Valid' });
  }
);
Enter fullscreen mode Exit fullscreen mode

Schema wildcard keys

When schema keys contain wildcards, quote the keys.

This matters for paths such as:

const schema = {
  'siblings.*.name': {
    notEmpty: true,
  },
};
Enter fullscreen mode Exit fullscreen mode

Complete Signup Example

A simple end-to-end example combines field selection, sanitization, validation, error handling, and matchedData().

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

const app = express();

app.use(express.json());

app.post('/signup',
  body('email')
    .trim()
    .isEmail()
    .withMessage('Bad e-mail'),

  body('password')
    .isLength({ min: 6 }),

  (req, res) => {
    const result = validationResult(req);

    if (!result.isEmpty()) {
      return res.status(400).json({
        errors: result.array(),
      });
    }

    const { email, password } = matchedData(req);

    // ... create the user

    res.json({
      email,
      password,
    });
  }
);
Enter fullscreen mode Exit fullscreen mode

The basic flow is:

Request
  ↓
Select field location
  ↓
Run validators / sanitizers / modifiers
  ↓
validationResult(req)
  ↓
If errors → return error response
  ↓
matchedData(req)
  ↓
Use validated/sanitized data
Enter fullscreen mode Exit fullscreen mode

Express-Validator Cheat Sheet

The basic recipe

  1. Choose the location: body(), query(), or param().
  2. Add validation chains.
  3. Run the request through the validators.
  4. Read errors with validationResult(req).
  5. Get validated/sanitized data with matchedData(req).

Validators

Validators check values.

Examples:

.isEmail()
.notEmpty()
.isLength({ min: 6 })
Enter fullscreen mode Exit fullscreen mode

Sanitizers

Sanitizers transform values.

Examples:

.trim()
.escape()
Enter fullscreen mode Exit fullscreen mode

Modifiers

Modifiers change chain behavior.

Example:

.optional()
Enter fullscreen mode Exit fullscreen mode

Field locations

body('field')
query('field')
param('field')
Enter fullscreen mode Exit fullscreen mode

Wildcards

*  → one path segment
** → any depth
Enter fullscreen mode Exit fullscreen mode

ContextRunner

Run a validation chain manually with:

await chain.run(req);
Enter fullscreen mode Exit fullscreen mode

ExpressValidator

Use the class when organizing custom validators and sanitizers for reuse.


Important Gotchas

1. Validators do not send responses

You still need to check:

validationResult(req)
Enter fullscreen mode Exit fullscreen mode

and decide what response to return.

2. Order matters

For example:

trim().notEmpty()
Enter fullscreen mode Exit fullscreen mode

and:

notEmpty().trim()
Enter fullscreen mode Exit fullscreen mode

can behave differently because the operations happen in the order they appear in the chain.

3. Chains are mutable

Be careful when reusing a validation chain. Factory functions can help create a fresh chain.

4. Custom sanitizers must return

A custom sanitizer needs to return the transformed value.

5. Use wildcards for array elements

Instead of:

siblings.name
Enter fullscreen mode Exit fullscreen mode

use:

siblings.*.name
Enter fullscreen mode Exit fullscreen mode

when siblings contains multiple objects.

6. Literal dotted keys need special handling

If the actual field name contains a dot, represent the literal key correctly, such as:

["a.b"]
Enter fullscreen mode Exit fullscreen mode

7. Quote wildcard keys in schemas

For checkSchema(), use quoted keys for wildcard paths:

'siblings.*.name'
Enter fullscreen mode Exit fullscreen mode

8. Prefer .if() for conditional validation

The notes recommend using .if() rather than manually running conditional validation logic.


Final Mental Model

Think about express-validator as a pipeline:

REQUEST
   │
   ├── body / query / params
   │
   ▼
FIELD SELECTION
   │
   ▼
VALIDATION CHAIN
   │
   ├── validators → check
   ├── sanitizers → transform
   └── modifiers  → change behavior
   │
   ▼
validationResult(req)
   │
   ├── errors → return response
   │
   └── valid
        │
        ▼
matchedData(req)
        │
        ▼
Use validated/sanitized data
Enter fullscreen mode Exit fullscreen mode

These notes cover the core concepts from express-validator v7.3.0: getting started, validation results, sanitization, matched data, validation chains, field selection, wildcards, custom validators and sanitizers, error messages, the ExpressValidator class, manual validation, and schema validation.

Top comments (0)