Are you overusing express-validator’s custom validator?

12 mins read

express-validator is a popular express middleware library used to validate and sanitize user input. It offers a set of express middlewares that warp over a vast collection of validators and sanitizers provided by validator.js. It also provides a way to define custom validators to handle specific validation requirements that are not covered by validator.js.

While custom validators should be reserved for special cases that involve complex validation rules, it’s very common to see them being used in places where a built-in/standard validator will suffice. This happens mainly because developers don’t know if a built-in/standard validator already exists for their use case or they just don’t know how to validate a specific input from its enclosing data structure (like a deeply nested object or an array). In this post, I will cover some built-in validators and sanitizers you never knew existed and how you can validate array inputs.

Standard Validators & Sanitizers

Before we start looking at some of the most commonly used validators and sanitizers that express-validator offers, let’s set up a small express application with a single endpoint for posting data.

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

const app = express();

app.use(express.json()); // Parse JSON requests

app.post("/submit", (req, res) => {
  res.status(200).json({ message: "Nothing to validate yet." });
});

app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

Standard Validators

isEmail()

This validator checks if a given input is a valid email address. It verifies that the input string conforms to the standard email format, including the presence of “@” and a valid domain.

Here’s how you can use the isEmail validator:

app.post(
  "/submit",
  // Validate that the 'email' field is a valid email address
  body("email").isEmail().withMessage("Invalid email address"),
  (req, res) => {
    const errors = validationResult(req);

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

    // If validation passes, you can access the valid 'email' value in req.body
    const { email } = req.body;

    // Process the data or send a success response
    res.status(200).json({ message: "Email is valid!", email });
  }
);

isUrl()

Validates that the input is a valid URL. It checks whether the string follows URL syntax rules, such as having a valid scheme (e.g., “http” or “https”).

Here’s how you can use the isURL validator:

app.post(
  "/submit",
  // Validate that the 'website' field is a valid URL
  body("website").isURL().withMessage("Invalid URL"), // Custom error message if validation fails
  (req, res) => {
    const errors = validationResult(req);

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

    // If validation passes, you can access the valid 'email' value in req.body
    const { website } = req.body;

    // Process the data or send a success response
    res.status(200).json({ message: "URL is valid!", website });
  }
);

These examples demonstrate how to use the isEmail and isURL validators to ensure that the ’email’ and ‘website’ fields in a POST request contain a valid email and URL.

isNumeric()

Verifies if the input is a numeric value. It checks whether the input contains only digits and optional signs like “+” or “-“.

isAlpha()

Ensures that the input contains only alphabetic characters (letters). It won’t pass if the input contains numbers, spaces, or special characters.

isAlphaNumeric()

Checks if the input contains only alphanumeric characters, which means letters and numbers. Special characters and spaces will cause this validator to fail.

Here’s how you can use these validators:

app.post(
  "/submit",
  [
    // Validate that the 'age' field is numeric.
    body("age").isNumeric().withMessage("Age must be a numeric value"),

    // Validate that the 'name' field contains only alphabetic characters.
    body("name")
      .isAlpha()
      .withMessage("Name must contain only alphabetic characters"),

    // Validate that the 'username' field contains only alphanumeric characters.
    body("username")
      .isAlphanumeric()
      .withMessage("Username must contain only alphanumeric characters"),

    // Other validation rules for additional fields...
  ],
  (req, res) => {
    const errors = validationResult(req);

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

    // If validation passes, you can access the valid values in req.body
    const { age, name, username } = req.body;

    // Process the data or send a success response
    res.status(200).json({ message: "Data is valid!", age, name, username });
  }
);

This example demonstrates how to use the isNumeric, isAlpha, and isAlphanumeric validators to ensure that the specified fields in a POST request meet the desired validation criteria.

isInt() & isFloat()

These validators verify if the input is a valid integer or a floating-point number, respectively. They ensure that the input can be parsed as the specified numeric type.

Here’s how you can use these validators:

app.post(
  "/submit",
  [
    // Validate that the 'age' field is an integer.
    body("age").isInt().withMessage("Age must be an integer"),

    // Validate that the 'height' field is a floating-point number.
    body("height")
      .isFloat()
      .withMessage("Height must be a floating-point number"),

    // Other validation rules for additional fields...
  ],
  (req, res) => {
    const errors = validationResult(req);

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

    // If validation passes, you can access the valid values in req.body
    const { age, height } = req.body;

    // Process the data or send a success response
    res.status(200).json({ message: "Data is valid!", age, height });
  }
);

This example demonstrates how to use the isInt and isFloat validators to ensure that the specified fields in a POST request are either integers or floating-point numbers, respectively.

isDate()

Validates that the input is a valid date. It checks if the input can be interpreted as a valid date using JavaScript’s Date object.

isLength()

Validates the length of a string. You can specify options like min and max to ensure the input’s length falls within a specific range.

isIn()

Checks if a value is included in a predefined list of allowed values. It’s particularly useful when you want to ensure that a specific input field’s value matches one of the expected values.

matches()

Checks if a string matches a specified regular expression pattern. This validator allows you to define custom validation rules based on regular expressions, giving you fine-grained control over the format or structure of the input data.

isBoolean()

Checks if the input is a boolean value, which should be either true or false.

Here’s how you can use the above validators in an express application.

app.post(
  "/submit",
  [
    // Validate that the 'birthdate' field is a valid date.
    body("birthdate").isDate().withMessage("Invalid date format"),

    // Validate that the 'username' field has a length between 5 and 15 characters.
    body("username")
      .isLength({ min: 5, max: 15 })
      .withMessage("Username must be between 5 and 15 characters"),

    // Validate that the 'country' field is one of the specified values.
    body("country")
      .isIn(["USA", "Canada", "UK", "Australia"])
      .withMessage("Invalid country"),

    // Validate that the 'code' field matches a specific pattern (e.g., 3 letters followed by 3 numbers).
    body("code")
      .matches(/^[A-Za-z]{3}\d{3}$/)
      .withMessage("Invalid code format"),

    // Validate that the 'isSubscribed' field is a boolean value.
    body("isSubscribed")
      .isBoolean()
      .withMessage("isSubscribed must be a boolean"),

    // Other validation rules for additional fields...
  ],
  (req, res) => {
    const errors = validationResult(req);

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

    // If validation passes, you can access the valid values in req.body
    const { birthdate, username, country, code, isSubscribed } = req.body;

    // Process the data or send a success response
    res.status(200).json({
      message: "Data is valid!",
      birthdate,
      username,
      country,
      code,
      isSubscribed,
    });
  }
);

This example demonstrates how to use various validators to ensure that different types of data in a POST request meet the desired validation criteria.

Standard Sanitizers

normalizeEmail()

Use to normalize an email address, making it consistent and suitable for validation or storage. It helps standardize email addresses by converting them to a consistent format, typically lowercasing the local part (username) and the domain part of the email address.

trim()

This sanitizer removes leading and trailing whitespace from a string. It’s useful for cleaning up input data before validation.

escape()

Escapes HTML characters in the input string, preventing potential cross-site scripting (XSS) vulnerabilities by converting characters like < and > into their HTML entity equivalents.

Here’s an example where we sanitize input data to ensure that email addresses are normalized, whitespace is trimmed, and HTML characters are escaped.

app.post(
  "/submit",
  [
    // Sanitize and normalize the 'email' field.
    body("email")
      .isEmail()
      .normalizeEmail()
      .withMessage("Invalid email address"),

    // Sanitize and trim leading/trailing whitespace from the 'username' field.
    body("username")
      .trim()
      .notEmpty() // Ensure the field is not empty after trimming
      .withMessage("Username is required"),

    // Sanitize and escape HTML characters in the 'comment' field.
    body("comment").escape().optional({ checkFalsy: true }), // Allow empty strings after escaping

    // Other validation rules for additional fields...
  ],
  (req, res) => {
    const errors = validationResult(req);

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

    // If validation passes, you can access the sanitized values in req.body
    const { email, username, comment } = req.body;

    // Process the data or send a success response
    res
      .status(200)
      .json({ message: "Data is valid!", email, username, comment });
  }
);

This example demonstrates how to use various sanitizers to ensure that different types of data in a POST request are properly sanitized and meet the desired format or security requirements.

Validating array inputs

Validating array inputs might seem challenging at first but it’s really not that different from validating other inputs.

Wildcards to the rescue

The *, also known as the wildcard allows us to apply the same rules to all items of an array, or all keys of an object. The wildcard will correctly select all indices of an array or keys of an object and validate/sanitize the input at that location independently from the others.

Here’s an example where we validate an array of users sent in the request body.

app.post(
  "/submit",
  [
    // Validate, sanitize, and normalize the 'email' fields.
    body("*.email")
      .isEmail()
      .normalizeEmail()
      .withMessage("Invalid email address"),

    // Validate, sanitize, and trim leading/trailing whitespace from the 'username' fields.
    body("*.username")
      .isAlphanumeric()
      .isLength({ min: 5, max: 15 })
      .trim()
      .notEmpty() // Ensure the field is not empty after trimming
      .withMessage(
        "Username is required and must be between 5 and 15 characters"
      ),

    // Validate that the 'website' fields are a valid URL
    body("*.website").isURL().withMessage("Invalid URL"),

    // Validate that the 'age' fields are an integer.
    body("*.age").isInt().withMessage("Age must be an integer"),

    // Validate that the 'height' fields are a floating-point number.
    body("*.height")
      .isFloat()
      .withMessage("Height must be a floating-point number"),

    // Validate that the 'name' fields contains only alphabetic characters.
    body("*.name")
      .isAlpha()
      .withMessage("Name must contain only alphabetic characters"),

    // Validate that the 'birthdate' fields are a valid date.
    body("*.birthdate").isDate().withMessage("Invalid date format"),

    // Validate that the 'country' fields are one of the specified values.
    body("*.country")
      .isIn(["USA", "Canada", "UK", "Australia"])
      .withMessage("Invalid country"),

    // Validate that the 'code' fields match a specific pattern (e.g., 3 letters followed by 3 numbers).
    body("*.code")
      .matches(/^[A-Za-z]{3}\d{3}$/)
      .withMessage("Invalid code format"),

    // Validate that the 'isSubscribed' fields are a boolean value.
    // If the 'isSubscribed' fields are valid boolean values (i.e. true/false or 1/0),
    // the value is sanitized and cast to a boolean type
    body("*.isSubscribed")
      .isBoolean()
      .bail()
      .toBoolean()
      .withMessage("isSubscribed must be a boolean"),

    // Sanitize and escape HTML characters in the 'comment' fields.
    body("*.comment").escape().optional({ checkFalsy: true }), // Allow empty strings after escaping
  ],
  (req, res) => {
    const errors = validationResult(req);

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

    // If validation passes, you can access the sanitized values in req.body
    const users = req.body.map(
      ({
        email,
        username,
        website,
        age,
        height,
        name,
        birthdate,
        country,
        code,
        isSubscribed,
        comment
      }) => {
        email,
        username,
        website,
        age,
        height,
        name,
        birthdate,
        country,
        code,
        isSubscribed,
        comment
      });

    // Process the data or send a success response
    res
      .status(200)
      .json({ message: "Data is valid!", users });
  }
);

This example demonstrates how to use wildcards to validate all items of an array in a POST request.

Conclusion

In conclusion, the use of custom validators in the express-validator library should be reserved for situations where built-in or standard validators are insufficient to meet specific validation requirements. This article has provided an in-depth exploration of various standard validators and sanitizers available in express-validator, along with practical examples for implementing them in an Express application. These standard validators cover a wide range of common validation scenarios, from checking email addresses and URLs to verifying numeric values, dates, and more.

Furthermore, the article has highlighted the powerful feature of wildcards, which allows you to apply the same validation and sanitization rules to all items within an array or keys within an object. This capability simplifies the validation of array inputs, making it straightforward to ensure that each element within an array adheres to the desired validation criteria.

By leveraging the extensive set of standard validators and wildcards provided by express-validator, developers can streamline the validation and sanitization process for user input, leading to more robust and secure Express applications. It is essential to choose the appropriate validation methods based on the specific data and validation requirements to maintain code clarity and efficiency.