Overview
When it comes to do with building backend applications, certain requirements are often considered which could impact greatly on the system. They include;
- Security
- Reliability &
- Intergrity of data.
The strict enforcement of these requirements could help prevent irregularities and loopholes in your application, like; inaccurate data from entering your system, to malicious users able to exploit vulnerabilities through techniques like SQL injection, cross-site scripting (XSS) and other forms of injection attacks.
It is against this backdrop we will be considering the adoption of data validation and sanitization tool like Express validator in Node.js to maintain data integrity and consistency across our system. However, it is important to note that Node.js has other powerful validation tools like;
- Joi
- Ow
- Superstruct
- Tv4
- Validator.js
- Fastest Validator
Why Express Validator?
Due to it's popularity and integration with Express and focus on web related tasks, we will be considering Express validator and subsequently x-ray Joi which appears to be a more sophisticated validation library for various data structures.
Express validator is an Express.js library that helps in sanitizing inputs and provides validation support for your backend application.
Installation and Setup
This article will give you a clear way of integrating Express validator in your Node application. First head to your terminal and type in the following:
npm i express-validator
This will install the Express validator library in your application and have it saved under the dependencies section of your package.json file. Next we create simple validation middleware functions that can be called when needed, this will be in a separate file called validators.js;
In the validator.js file, we use the following snippet:
import * as validators from "express-validator";
import User from "../../models/users/userModel";
export const registrationRules = () => {
return [
body("firstName", "Firstname is required")
.notEmpty()
.withMessage("Firstname must be at least 3 chars long"),
body("lastName", "Lastname is required")
.notEmpty()
.withMessage("Lastname must be at least 3 chars long")
]
}
From the above function, you notice that we have imported 2 items validators from express-validator and User which represents the user collection which we may likely want to lookup data on the database to verify incoming requests.
Also, we have defined the registrationRules function returning an array of data. So far, the only validation function used is the notEmpty() method which ensures the firstName and lastName fields are required. We could go on further to add more validation methods which we refer to as method chaining like;
export const registrationRules = () => {
return [
body("firstName", "Firstname is required")
.notEmpty()
.trim()
.escape()
.isLength({ min: 3 })
.withMessage("Firstname must be at least 3 chars long"),
body("lastName", "Lastname is required")
.notEmpty()
.trim()
.escape()
.isLength({ min: 3 })
.withMessage("Lastname must be at least 3 chars long")
]
}
We added 3 more validation functions; trim(), escape() and isLength(), to remove whitespaces from input, escape HTML tags in inputs and to specify the length of characters required per input supplied. For more validation methods, see Express validation chains.
In a real world scenario, we may want to validate email addresses, to check the right format and also verify if the email already exist in our database, then we return an error or whatever we intend to do with that response. We can achieve that by doing the following:
In the registrationRules() function above, we add the email field like this:
export const registrationRules = () => {
return [
body("email", "Email is required")
.trim()
.escape()
.isEmail()
.withMessage("Please provide a valid email")
.custom(async (email) => {
return User.findOne({ email}).then((user) => {
if (user) {
return Promise.reject("E-mail already in use");
}
});
})
.withMessage("Account is already registered"),
body("firstName", "Firstname is required")
.notEmpty()
.trim()
.escape()
.isLength({ min: 3 })
.withMessage("Firstname must be at least 3 chars long"),
body("lastName", "Lastname is required")
.notEmpty()
.trim()
.escape()
.isLength({ min: 3 })
.withMessage("Lastname must be at least 3 chars long"),
]
}
The custom method above, helps us make an asynchronous request to the server to check if the email value provided has been used previously. If true, we get an eror response. This operation is often referred to as Custom validation.
Now, we need to start collecting all errors so we can display them to the client whenever these errors occur. We then create the middleware still in our validator.js file like;
export const validateRequests = (req, res, next) => {
const errors = validationResult(req);
const clientErrors = [];
errors.array().map((err) => clientErrors.push({ message:
err.msg }));
if (clientErrors.length == 0) {
return next();
}
return res.status(400).send({
clientErrors,
});
};
In the validateRequests middleware above, we have called on the validationResult method. It wraps the req object which stores the errors in the "errors" variable and all we simply do is extract the object containing the error message into the clientErrors array.
We then set status to 400 and send the clientErrors array to client. When we put all together, the validator.js file will then look like this;
import * as validators from "express-validator";
import User from "../../models/users/userModel";
export const registrationRules = () => {
return [
body("email", "Email is required")
.trim()
.escape()
.isEmail()
.withMessage("Please provide a valid email")
.custom(async (email) => {
return User.findOne({ email}).then((user) => {
if (user) {
return Promise.reject("E-mail already in use");
}
});
})
.withMessage("Account is already registered"),
body("firstName", "Firstname is required")
.notEmpty()
.trim()
.escape()
.isLength({ min: 3 })
.withMessage("Firstname must be at least 3 chars long"),
body("lastName", "Lastname is required")
.notEmpty()
.trim()
.escape()
.isLength({ min: 3 })
.withMessage("Lastname must be at least 3 chars long"),
]
}
export const validateRequests = (req, res, next) => {
const errors = validationResult(req);
const clientErrors = [];
errors.array().map((err) => clientErrors.push({ message:
err.msg }));
if (clientErrors.length == 0) {
return next();
}
return res.status(400).send({
clientErrors,
});
};
One good benefit of having the validateRequests middleware written this way is that, it can be reusable across the entire codebase for extracting any error related tasks based on the rules which will likely change. For instance, we may need to define signinRules() after registrationRules() then validateRequests middleware stays same.
Finally, we can then use our validateRequests() middleware in the usersRouter where we handle the registration API request like this:
import express from "express";
import {registrationRules, validateRequests} from
'../..validator.js'
const usersRouter = express.Router();
usersRouter.post("/register", registrationRules(),
validateRequests, async (req, res) => {
try{
const {email, firstName, lastName} = req.body;
res.status(200).send({message: "Registration was successful" userData: {email, firstName, lastName} });
}catch (error){
console.error(error)
res.status(500).send({ error: error.message });
}
});
From the above definiton, our validation middleware are now active and when client send an API request to the /register endpoint above, the request is intercepted and we see the corresponding errors.
Wrapping up
Congratulations!! You have finally completed the process of validating and sanitizing API requests using Express validator. However, this article is a guide to have you all set up and running. For detailed Express validator methods to help enrich your application needs, please don't hesitate to look up the Express Validation Docs.
I will love to see what you build next using Express Validator in your projects.
If you encountered any challenge while flowing with this article, please don't hesitate to let me know in the comment section. Catch me on LinkedIn.
Top comments (0)