DEV Community

Cover image for How to use Joi Validator in Node + Express Applications. Do it the right way
Jeffrey Nwankwo
Jeffrey Nwankwo

Posted on

How to use Joi Validator in Node + Express Applications. Do it the right way

Joi is widely considered as the most powerful library for describing schemas and validating data in JavaScript. When it comes to Nodejs applications, especially those built with Express, Joi offers a simple yet flexible API for defining and validating different types of data like HTTP request parameters, query parameters, request bodies, and more.

Personally, I've utilized Joi to define validation rules for various data types and effortlessly validate incoming data against them. Joi also offers a diverse range of validation methods that can be customized and combined to cater to specific validation needs. Moreover, it comes with error handling and reporting mechanisms that aid developers in identifying and handling validation errors in a concise and clear manner.

This isn't a comprehensive article on all things Joi validators. Instead, it focuses on how to utilize it like an expert in a Node + Express application.

For instance, when dealing with a small application, one can validate the request body in a Node/Express application with Joi by creating a validation schema and utilizing it to validate the request body inside the route handler. All the schemas can be stored in a schemas.ts file and then imported to the page where request body validation is required.

const Joi = require('joi');
const express = require('express');
const app = express();

// Define a validation schema for the request body
const schema = Joi.object({
  username: Joi.string().alphanum().min(3).max(30).required(),
  password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required(),
});

// Add a route handler to validate the request body
app.post('/login', (req, res, next) => {
  const { error, value } = schema.validate(req.body);
  if (error) {
    return res.status(400).json({ error: error.details[0].message });
  } else {
    // Valid data, continue with login logic
    // ...
  }
});

Enter fullscreen mode Exit fullscreen mode

While it may seem like an easy way to do things, importing validation schemas and validating request bodies inside request handlers is not the best approach, especially when working on medium to large projects with other developers. Over time, this can lead to code that is cluttered and repetitive.

But don't worry, there is a better approach that I'll show you. To follow along, you can use this starter template I've created. It's a simple setup for a Node + Express + Typescript application that is ready for us to implement the Joi validator.

Grab the starter template here

After downloading the files, the first step is to run the npm install command to install all the dependencies. Once that is complete, run the npm run dev command to start the server. Before proceeding, please verify that everything is working properly. You should see the following message when you open localhost:5000 in your browser:

Server running on port 5000

Now that we have everything set up, we can move on to implementing validation with Joi in our application. To get started, install the Joi npm package by running the command npm install joi in your terminal.

🚧 Our Approach 🚧

Before we start implementing validation with Joi, let's take a moment to discuss the approach we'll be using. We will create a file called schemas.ts to store all the schemas that our entire application will use. Inside this file, we will export an object where each key/value pair will be the route/path and the corresponding Joi schema to be validated.

Next, we will create a middleware called SchemaValidator to validate our request bodies. The SchemaValidator middleware will accept two arguments. The first argument will be the path (route), and the second argument will be an option to use a custom error message. With the path, we can get the corresponding schema and validate the request body against it.

Create the schemas.ts file inside the src folder and paste this code.
schemas.ts

import Joi, { ObjectSchema } from "joi";

const PASSWORD_REGEX = new RegExp(
  "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!.@#$%^&*])(?=.{8,})"
);

const authSignup = Joi.object().keys({
  firstname: Joi.string().required(),
  lastname: Joi.string().required(),
  email: Joi.string().email().required(),
  password: Joi.string().pattern(PASSWORD_REGEX).min(8).required(),
});

const authSignin = Joi.object().keys({
  email: Joi.string().required(),
  password: Joi.string().required(),
});

export default {
  "/auth/signin": authSignin,
  "/auth/signup": authSignup,
} as { [key: string]: ObjectSchema };

Enter fullscreen mode Exit fullscreen mode

Here, we define two validation schemas, authSignup and authSignin. These schemas ensure that the input data conforms to specific rules, such as requiring certain fields, ensuring they are of a certain type, and/or conform to specific patterns. The exported object containing the schemas will be used in our SchemaValidator middleware.

If you'd like to learn more about Joi, here's a link to the documentation: https://joi.dev/api/?v=17.9.1

Next, create a new folder inside the src folder and call it middleware. Inside the middleware folder, create schemaValidator.ts file and paste the code below:

import { RequestHandler } from "express";
import schemas from "../schemas";

interface ValidationError {
  message: string;
  type: string;
}

interface JoiError {
  status: string;
  error: {
    original: unknown;
    details: ValidationError[];
  };
}

interface CustomError {
  status: string;
  error: string;
}

const supportedMethods = ["post", "put", "patch", "delete"];

const validationOptions = {
  abortEarly: false,
  allowUnknown: false,
  stripUnknown: false,
};

const schemaValidator = (path: string, useJoiError = true): RequestHandler => {
  const schema = schemas[path];

  if (!schema) {
    throw new Error(`Schema not found for path: ${path}`);
  }

  return (req, res, next) => {
    const method = req.method.toLowerCase();

    if (!supportedMethods.includes(method)) {
      return next();
    }

    const { error, value } = schema.validate(req.body, validationOptions);

    if (error) {
      const customError: CustomError = {
        status: "failed",
        error: "Invalid request. Please review request and try again.",
      };

      const joiError: JoiError = {
        status: "failed",
        error: {
          original: error._original,
          details: error.details.map(({ message, type }: ValidationError) => ({
            message: message.replace(/['"]/g, ""),
            type,
          })),
        },
      };

      return res.status(422).json(useJoiError ? joiError : customError);
    }

    // validation successful
    req.body = value;
    return next();
  };
};

export default schemaValidator;

Enter fullscreen mode Exit fullscreen mode

The SchemaValidator function takes in a path string and a boolean flag as parameters and returns an Express middleware function.

The middleware function validates the request body against a predefined schema (the schemas in our schemas.ts file) using the Joi library. If the validation fails, it returns a 422 HTTP status code along with a custom error object that contains either the Joi error or a generic error message, depending on the value of the useJoiError flag.

If the validation succeeds, it sets the request body to the validated value and calls the next() function to pass control to the next middleware. Pretty simple.

Now that we have set up our approach, we can use the SchemaValidator middleware in our route definitions. For example, here's how we can use it to validate the auth routes.

First, create a new folder called auth inside the routes folder. Inside the auth folder, create an index.ts file where we will define the two authentication routes: the signup and signin routes.

import { Router, Request, Response } from "express";

const router = Router();

router.post("/signin", (req: Request, res: Response) => {});

router.post("/signup", (req: Request, res: Response) => {});

export default router;

Enter fullscreen mode Exit fullscreen mode

Then import the auth routes in the main routes/index.ts file.

import { Router, Request, Response } from "express";

import authRoutes from "./auth";

const router = Router();

router.get("/api/v1", (req: Request, res: Response) => {
  res.send("Hello Dev Community!");
});

//? Import other routes here

router.use("/api/v1/auth", authRoutes);

//* eg: router.use("/api/v1/user", userRoutes);

export default router;
Enter fullscreen mode Exit fullscreen mode

Back to the auth routes file, uur route handler will require some data from the user to process, this is where we first validate the user input before it gets to our handler.

To use the SchemaValidator middleware, import it into the file, and apply it as a middleware function just before the route handlers for the "signin" and "signup" routes. Use the schemaValidator function and pass the path associated with the correct schema, similar to what was done in the schemas.ts file.

The updated code will look something like this:

import { Router, Request, Response } from "express";

import schemaValidator from "../../middleware/schemaValidator";

const router = Router();

router.post(
  "/signin",
  schemaValidator("/auth/signin"),
  (req: Request, res: Response) => {
    return res.send("You've successfully logged in βœ”");
  }
);

router.post(
  "/signup",
  schemaValidator("/auth/signup"),
  (req: Request, res: Response) => {
    return res.send("Sign up complete βœ”");
  }
);

export default router;

Enter fullscreen mode Exit fullscreen mode

Now that we have defined our authentication routes, we can test them using Postman. Start your dev server by running npm run dev. To test the signup route, enter localhost:5000/api/v1/auth/signup in Postman and set the request method to POST and the request body type to raw/JSON. If we make the request without entering anything, we will get an error response like this:

Sign up route on PostMan

And if we enter all the required fields correctly, we should receive a response with the message "Sign up complete βœ”".

Sign up route on PostMan

You can also test the signin route and experiment with different payloads to see the different error messages that can be generated.

With this implementation, we can easily create more routes and schemas in our application without worrying about validating each schema. All we need to do is create the schema in the schemas.ts file and apply the SchemaValidator middleware wherever we want to validate the schema. This approach helps to keep our code organized and avoids code repetition.

How exciting right?

It is important to follow standard conventions and organize your application logic in the service and controller folders. In this example, however, we did not follow that convention because it was not the focus of this tutorial.

Summary

The approach we discussed here makes the process of validating data in our Node.js applications easier and more efficient. By using a centralized schema validation approach, we avoid repetitive code and ensure that our code remains clean and easy to understand. Additionally, this approach makes it easy for other developers to pick up our code and understand it.

It is worth noting that this approach can also be applied to other validation libraries in Node.js, such as Zod. Overall, this approach can save us a lot of time and make our codebase more maintainable. Happy coding, and feel free to leave any thoughts or questions in the comments section.

That's the end of the guide! You can access the complete code by visiting https://github.com/JeffreyChix/joi-node-schema-validation.

For more content like this, follow me on Twitter @JeffreySunny1. My DM is open.

Top comments (0)