Accepting untested and unvalidated data into a web application can cause security vulnerabilities, and unforeseen issues can arise from the invalid data.
Node.js ORMs, such as Sequelize and TypeORM, allow you to set validation rules out of the box at the application level. During API development, data comes from HTTP requests to specific endpoints. This happens at the request level; thus, the default validation offered by the ORMs doesn't apply to them.
Joi is a schema description and data validator for JavaScript. Here, you will learn how to use the Joi validation library to validate data at the request level.
Setting Up Demo Project
To demonstrate how Joi validates data, you will build a simple demo application that mimics an actual application.
First, create a project folder and move into it by running the following command:
mkdir demoapp && cd demoapp
Next, initialize npm in your project directory by running:
npm init -y
Next, you will need to install some dependencies. The dependencies required for this tutorial include the following:
- Express: Express is a Node.js framework that provides a robust set of features for web and mobile applications. Express makes it easier to build backend applications with Node.js.
- Joi: Joi is a data validation library for Node.js.
Install the dependencies with the node package manager by running the command below:
npm install express joi
Next, create an index.js file in your root directory and add the following code block to it:
const express = require("express");
const router = require("./routes");
const port = 3000;const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(router);
app.listen(port, () => {
console.log("app listening on port 3000!");
});
The code block above sets up a simple Express server. It configures middleware to parse incoming request data and handle incoming requests and starts the server to listen for incoming requests on port 3000.
Routing and Handling Requests
For simplicity, you will create a request handler middleware that returns a status code, along with the request body, as a response to every request that attempts to send data to your application.
Create a handler.js file in your project’s root directory and add the code block below:
const demoHandler = (req, res, next) => {
res.send({
code: 201,
data: req.body,
});
next();
};module.exports = demoHandler;
Next, create a router.js file in your project’s root directory and add the code block below to your file:
const express = require("express");
const demoHandler = require("./handler");
const router = express.Router();router.post("/signup", demoHandler);
module.exports = router;
Creating a Joi Schema
A Joi schema represents a specific data object's expected structure and validation rules.
To create a Joi schema, you can use the Joi.object() method and chain various validation rules exposed by Joi to define the structure and validation requirements for your data.
For example:
const exampleSchema = Joi.object({
name: Joi.string().min(3).required(),
});
The example above describes a simple Joi schema with a name property. The name property has a value of Joi.string().min(3).required(). This means that the name value should be a string, with a minimum length of 3 characters, and it is required.
Using Joi, you can chain various methods to add more validation constraints to each field defined in your schema.
Here’s an example with more fields and validation constraints:
const userSchema = Joi.object({
email: Joi.string().email().required(),password: Joi.string().min(6).required(),
age: Joi.number().min(18).optional(),
employed: Joi.boolean().optional(),
phone: Joi.string()
.regex(/^\\d{3}-\\d{3}-\\d{4}$/)//"123-456-7890"
.required(),
address: Joi.object({
street: Joi.string().min(3).required(),
city: Joi.string().min(3).required(),
state: Joi.string().min(3).required(),
zip: Joi.number().min(3).required(),
}).required(),
hobbies: Joi.array().items(Joi.string()).required(),
}).options({ abortEarly: false });
The userSchema defines the following constraints for each property:
- email: Must be a valid email string.
- password: Must be a string with a minimum of 6 characters.
- age: An optional number with a minimum value of 18.
- employed: An optional boolean.
- phone: A required string that matches the specified regex expression (/^\d{3}-\d{3}-\d{4}$/).
- address: An object representing the user's address with the following sub-properties.
- street: A required string with a minimum length of 3 characters.
- city: A required string with a minimum length of 3 characters.
- state: A required string with a minimum length of 3 characters.
- zip: A required number with a minimum value of 3.
- hobbies: A required array of strings.
In addition to the constraints, userSchema sets the abortEarly option to false. By default, Joi stops the execution of the program as soon as it encounters the first error and prints the error to the console. However, setting this option to false ensures that Joi checks the entire schema and prints all the encountered errors to the console.
Validating Data With Joi
Create a validation.js file and add the userSchema code to it.
Like so:
//validation.js
const Joi = require("joi");const userSchema = Joi.object({
//...
}).options({ abortEarly: false });
module.exports = userSchema;
Then, create a middleware that intercepts request payloads and verifies them against a provided schema by adding the following code below the userSchema code.
const validationMiddleware = (schema) => {
return(req, res, next) => {
const { error } = schema.validate(req.body);if (error) {
// Handle validation error
console.log(error.message);
res.status(400).json({ errors: error.details });
} else {
// Data is valid, proceed to the next middleware
next();
}
};
};
When a request is made, the middleware invokes the validate method of the schema to validate the request body. If any validation errors occur, the middleware sends a 400 Bad Request response with the error messages extracted from the validation error details.
On the other hand, if the validation passes without errors, the middleware calls the next() function.
Finally, export the validationMiddleware and userSchema.
module.exports = {
userSchema,
validationMiddleware,
};
Testing Validation Constraints
Import validationMiddleware and userSchema into your router.js file and set up the middleware like so:
const { validationMiddleware, userSchema } = require("./validation");router.post("/signup", validationMiddleware(userSchema), demoHandler);
Start your application by running the command below:
nodeindex.js
Then, make an HTTP POST request to localhost:3000/signup using the test data below. You can achieve this using cURL or any other API client.
{
"email": "user@example", // Invalid email format
"password": "pass", // Password length less than 6 characters
"age": 15, // Age below 18
"employed": true,
"hobbies": ["reading", "running"],
"phone": "123-456-789", // Invalid phone number format
"address": {
"street": "123",
"city": "Example City",
"state": "Example State",
"zip": 12345
}
}
This request would fail and return an error object as the payload contains a lot of invalid fields, such as email, password, age, and phone. Using the provided error object, you can handle the errors appropriately.
Simplifying Data Validation With Joi
Here you covered most of the basics of data validation using Joi. However, you can cover more advanced techniques and constraints in the Joi documentation.
Joi simplifies the data validation task in JavaScript, providing an intuitive solution that significantly improves the reliability and integrity of data stored in your application.