Wednesday, 3 February, 2021 UTC


Summary

Introduction

It is imperative to carry out server-side validation when building applications - especially client-facing applications. The reason being, one can never rely on the user’s input alone; as these inputs sometimes contain bogus/malicious data.
Client-side validation is a great way to sift through most of the input, but you still need to perform server-side validation as well.
There are many ways to validate data in Node.js and in this article, we will be taking a look at express-validator. Express-validator is a library which wraps around validator.js and exposes its functions as a set of middlewares.

Project Setup

For this tutorial, we will be building a demo backend server to mock user registration and login with Node.js. These fields will enforce certain rules and we'll validate the data that comes through.
Please note we will not be handling actual user registration and login logic i.e saving user data and implementing authentication as this is outside the scope of this article.
To get started, we will create a project folder, navigate into it and initialize it:
# Create the project folder
$ mkdir express-validator-tut

# Navigate into the project folder
$ cd express-validator-tut

# Initialize project
$ yarn init -y
# OR
$ npm init -y
When done, we will install the following dependencies by running the command below:
$ yarn add body-parser express express-validator
# OR
$ npm i body-parser express express-validator
Let's take a look at what we've installed:
  • express: A lightweight web application framework for Node.js. We will be using this to handle routing in our backend server.
  • body-parser: A middleware which will help us parse incoming request inputs (user inputs) to the req.body object.
  • express-validator: The library which we will be using to handle incoming input validation.
Lastly, we will create an index.js file in our project directory to host the boilerplate code for the instantiation of an Express application/server:
// index.js
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const port = 2022;

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

app.post('/register', (req, res) => {});
    
app.listen(port);
console.log('See where it all happens at http://localhost:'+port);
Now, let's run this app with node:
$ node index.js 
If all goes well, your terminal should output something along the lines of:

Standard Validation Rules with express-validator

In this section, we will learn how to add simple validation and sanitisation rules to incoming requests. Firstly, we want to check if the value input into the email field is a valid email or not. Then, we'll want to enforce that the password contains at least 6 characters.
To get started, let's add a couple of middleware functions to our /login route:
// index.js
...
const { body, validationResult } = require('express-validator');

app.post('/login',
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({
        min: 6
    }),
    (req, res) => {
        const errors = validationResult(req);

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

        res.status(200).json({
            success: true,
            message: 'Login successful',
        })
    });
...
In the snippet above, we are making use of two validator methods:
  • isEmail(): This validator function checks if the incoming string is a valid email address.
  • isLength(): This validator checks if the length of a string falls in a specified range. In our case, the range specified is a minimum of 6 characters.
Some of the other methods we could've used are:
  • isNumeric() - Checks if the input is numeric
  • contains() - Checks if the input contains a certain value
  • isBoolean() - Check is the input is a boolean value
  • isCurrency() - Checks if the input is currency-formatted
  • isJSON() - Checks if the input is JSON
  • isMobilePhone() - Checks is the input is a valid mobile phone number
  • isPostalCode() - Checks if the input is a valid postal code
  • isBefore() and isAfter() - Checks if a date is before or after another date
There are others, but these are probably the ones to cover most of your validation needs.
To ensure email addresses supplied by the user is free of noise and irregularities we will add a sanitiser to our email field as seen in the snippet above. The normalizeEmail() method helps to convert the emails entered into the standard approved format. This means if a user enters, for example, [email protected], it will be canonicalised to [email protected].
Validator.js offers some flexibility as this option can be toggled off or on but is set to on by default. There's a bunch of options for normalization you might want to check out if you're planning on normalizing the input. If you'd like to read more about other validators/sanitizer functions, you can check out Validator.js' official documentation.
Let's test out our code by sending a request with an invalid password, and a @googleemail.com email, using Postman or curl:
So far, we took a look at how we could validate incoming inputs for a sample login endpoint. Let's now switch to the registration endpoint and cover tasks like custom validation rules, error messages, schema validation and standardization of validation messages.

Custom Validation Rules and Error Messages with express-validator

To get started, let’s create our user registration endpoint by adding the following snippet to our index.js file:
// index.js
...
app.post('/register', (req, res) => {
    // Validate incoming input
    res.status(200).json({
        success: true,
        message: 'Registration successful',
    });
});
...

custom() Method

To make sure our users input unique usernames during registration, we can't use the standard methods wrapped from Validator.js methods, since there is no method to check it.
We'll have to write a custom validator for this, which can be done using the custom() method. The custom() method accepts a function, which can additionally be async. If the function is async, you'll want to reject the promise if the validation fails, and specify a custom message. If not, you can throw an exception.
Let's start with rejecting a promise first:
// index.js
...
app.post('/register',
    body("username").custom(value => {
        return User.find({
            username: value
        }).then(user => {
            if (user.length > 0) {
                // Custom error message and reject
                // the promise
                return Promise.reject('Username already in use');
            }
        });
    }),
    (req, res) => {
        // Validate incoming input
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            return res.status(400).json({
                errors: errors.array()
            });
        }
        ...
    })
In the code snippet above, we are calling the find() method on the User model Mongoose schema to check if the username entered by the client already exists in our database.
If it's present, we reject the promise with a message we'd like to return back to the user.
Although MongoDB will automatically detect this if the username field was marked unique when specifying the database schema. It’s advisable to handle this before it gets to the DB so our application does not crash prematurely.
Alternatively, you can throw an exception as a way to signify invalid input:
// index.js
...
    
app.post('/register',
        body("username").custom(value => {
            return User.find({
                username: value
            }).then(user => {
                if (user.length > 0) {
                    throw ("Username is taken!"); //custom error message
                }
            });
        }),
...

withMessage() Method

The second way to implement custom validation error messages is by using the withMessage()chain. You can put a number of validators, followed by withMessage() chained methods to specify error messages for each validation:
body("parameter")
    .validator1()
    .withMessage('Message 1')
    .validator2()
    .withMessage('Message 2')
Let's apply this with actual methods to our example:
// index.js
    
...
app.post('/register',
    body("password").isStrongPassword({
        minLength: 8,
        minLowercase: 1,
        minUppercase: 1,
        minNumbers: 1
    })
    .withMessage("Password must be greater than 8 and contain at least one uppercase letter, one lowercase letter, and one number"),
    (req, res) => {
        // Validate incoming input
    })
...
Let's make another request, with an invalid password and a username that's already in use:

Schema Validation with express-validator

Schema validation offers a cleaner approach to validating data. Instead of calling numerous functions, we specify the validation rules for each field and pass the schema into a single middleware function called checkSchema().
In the snippet below, we will be creating a validation schema for user registration endpoint:
// index.js
... 
const {body, checkSchema, validationResult} = require('express-validator');
const registrationSchema = {
    username: {
        custom: {
            options: value => {
                return User.find({
                    username: value
                }).then(user => {
                    if (user.length > 0) {
                        return Promise.reject('Username already in use')
                    }
                })
            }
        }
    },
    gender: {
        notEmpty: true,
        errorMessage: "Gender field cannot be empty"
    },
    password: {
        isStrongPassword: {
            minLength: 8,
            minLowercase: 1,
            minUppercase: 1,
            minNumbers: 1
        },
        errorMessage: "Password must be greater than 8 and contain at least one uppercase letter, one lowercase letter, and one number",
    },
    phone: {
        notEmpty: true,
        errorMessage: "Phone number cannot be empty"
    },
    email: {
        normalizeEmail: true,
        custom: {
            options: value => {
                return User.find({
                    email: value
                }).then(user => {
                    if (user.length > 0) {
                        return Promise.reject('Email address already taken')
                    }
                })
            }
        }
    }
}
...
By specifying a schema, we can drill into specific input fields to apply validators and sanitizers, and it's much more readable than chaining a lot of methods with validation messages like we've seen in the previous sections.
Now, we can go ahead and use this checkSchema() to validate data on registration:
app.post('/signup', checkSchema(registrationSchema), (req, res) => {
    // Validate incoming input
    const errors = validationResult(req);

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

    res.status(200).json({
        success: true,
        message: 'Registration successful',
    });
})
If you only need a small amount of validations and want to keep it simple, you can use methods. If you have a huge amount of validations to be done, it'll be more readable if you use schema validation.

Standardizing Validation Responses with express-validator

express-validator makes it possible to standardise validation error responses. This means you can create your middleware functions to run validations and handle validation errors.
An example of how this can be done is by creating a validate() function which will accept all our validators and run them in parallel using Promise.all():
// index.js
const validate = validations => {
    return async (req, res, next) => {
        await Promise.all(validations.map(validation => validation.run(req)));

        const errors = validationResult(req);
        if (errors.isEmpty()) {
            return next();
        }

        res.status(400).json({
            errors: errors.array()
        });
    };
};
Now our validate function has been created we can re-use it on multiple routes. Let's apply it to our login and registration routes:
// index.js
...
app.post('/login', validate([
        body('email').isEmail().normalizeEmail(),
        body('password').isLength({
            min: 12
        })
    ]),
    (req, res) => {
        // Process data
        res.status(200).json({
            success: true,
            message: 'Login successful',
        })
    });

app.post('/register', validate(checkSchema(registrationSchema)), (req, res) => {
    // Process data
    res.status(200).json({
        success: true,
        message: 'Registration successful',
    });
});
...
As seen in the snippet above - the use of a custom validation middleware which runs all our validators and sanitizers not only a gives us a performance boost with the Promise.all() call, but we also get to improve code readability. This will prove useful when we need to validate a lot of form fields.

Conclusion

In this article, we've gone over the basic and more advanced usage of express-validator, a great lightweight library that wraps around the well-known validator.js library.