Rest API input validation in nodejs

rest api input validation in nodejs

Most of the time when we are developing a Rest API it’s often a requirement to accept user inputs in the form of a request body. So to ensure that our API works as intended the input data must conform to a certain predefined format. Sometimes the erroneous inputs provided by our users are unintentional while other times they may be malicious. Therefore it becomes very important for us to perform proper input validation in our Rest API before we even start performing any serious operation on it like saving the data to a database or authenticating a user to our system.

Agenda

So, in this tutorial, we will be having a look at how we can implement a robust and powerful input validation system for the request body of our Rest API in nodejs using Joi’s inbuilt validators. Now there are a lot of popular packages available to choose from like express-validator which works quite well but we would like to introduce to you an easy yet powerful package known as Joi using which, we can implement advanced & robust data validation in our project without breaking a sweat. So let’s start, shall we?

For this tutorial, we will be implementing 3 validations starting from basic validation to advance one. We have implemented the validators as express middlewares so that we can easily use them in our rest API.

  • registrationValidator
  • messageValidator
  • todoTaskValidator

So without any further due, let’s start.

Install Joi

npm i joi

Rest API input validation directory structure

┣ validations/
┃ ┣ index.js
┃ ┣ message.js
┃ ┣ registration.js
┃ ┗ todoTask.js
┣ app.js
┣ package-lock.json
┗ package.json

As we can see above our app.js is the entry file of our app and all our validation files will go in the validation directory. Inside the validation directory, we have an index.js that auto imports all our validations so that we don’t have to import each one of the validation files separately one by one.

index.js

This file auto imports all our Joi validations used in our rest API and also handles Joi related errors.

const Joi = require('@hapi/joi')
const options = {
  abortEarly: false, // include all error
  allowUnknown: true, // ignore unknown props
  stripUnknown: true // remove unknown props
}

const fs = require('fs')
const path = require('path')
const basename = path.basename(__filename)
fs.readdirSync(__dirname).filter(file => {
  return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js')
}).forEach(file => {
  file = path.basename(file, '.js')
  module.exports[`${file}Validator`] = async (req, res, next) => {
    try {
      req.body = await require(path.join(__dirname, file))(Joi).validateAsync(req.body, options)
      return next()
    } catch (error) {
      if (error.isJoi) {
        error.status = 422
        return next(error)
      } else next(error)
    }
  }
})

Input validation examples for our rest api

The best way to learn anything is through examples so, we will do exactly that and will walk you through 3 Joi validation example use-case scenarios. Moreover, for the separation of concerns for each validation, we will create a separate validation file inside our validations directory.

registration.js

Let’s suppose we have an API route for our basic signup on our website when we receive users’ signup data in the request body and we have the below requirements.

Requirements

  • userName: must be a string, with minimum of 5 characters and maximum of 30 characters.
  • email: must be a string and valid email.
  • password: must be a string, minimum of 8 characters, must contain atleast a numeric, a capital alphabet, a small alphabet and a special character.
  • confirmPassword: must be same as our password.

    required is to say that the key must be present.

    pattern is use to match our value against a regex pattern.

    ref is to refer to another key in our validation schema

userName, email, password & confirmPassword are required.

// registration validator, index.js will automatically pass on Joi when calling this validator
module.exports = Joi => {
  return Joi.object({
    userName: Joi.string().min(5).max(30).required(),
    email: Joi.string()
      .email({ minDomainSegments: 2, tlds: { allow: ["com", "net"] } })
      .required(),
    password: Joi.string()
      .pattern(
        new RegExp(
          "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[[email protected]#$%^&*])(?=.{8,})"
        )
      )
      .required(),
    repeatPassword: Joi.ref("password"),
  });
}

Now that we have created our registration validation we can use it as a middleware in our /sign-up API route like below.

const { registrationValidator } = require("./validation");
app.post("/sign-up", registrationValidator, (req, res) => {
  res.status(200).json(req.body);
});

message.js

Let’s move on to our next example where let’s say we are implementing a basic chat system where a user can send a message and we must validate the message before processing it or sending it to the intended receiver.

Requirements

  • parentId: it is a messageId of the parent message to which this message is reply to and we also know that not every message will have a parentId(or to say not every message will be a direct reply to another message).
parentId: Joi
          .number()         //must be a number
          .default(null)    // define null as default value in case parentId key is not available.
          .allow(null)      // can also be able to accept null as value if passed
  • default is to define the default value of a key if not not available while allow is to say that this key accepts “null” as a value too if passed.
  • userId: receiver’s user Id.
userId: Joi
        .number()     // must be a number
        .required()   // key is required and must be passed, null value will work too
  • channelId: a message is either associated with a userId(receiver’s userId) or a group(channelId)
channelId: Joi
           .number()     // must be a number
           .required()    // key is required and must be passed
           .when("userId", { 
              not: null,
              then: Joi
                    .optional()
                    .allow(null)
           })

// if userId is not null(number) that means the message is sent to a user and not to a group. In that case channelId is optional(key is not requird to be passed) and if passed then it is allowed to have a null value.
  • when is use to apply conditional Joi validation schema based on key name, reference or a Joi
    schema
  • type: let’s say we allow our message type to be one of the 5 value namely text, image, video, file, audio.

type: Joi
      .valid("text", "image", "video", "file", "audio")  
                          // must me any one of the 5 types
      .default("text")    // if key is not passed, default value is text

valid is to define which values we accept as valid values.

  • message:
message: Joi
         .string()                         // must be a string if passed
         .when("type", {
            is: "text",                    // if message is of type text
            then: Joi.required(),          // then message key must be passed
            otherwise: Joi                 // otherwise
                       .optional() // is optional(key is not required to be passed)
                       .default("")  // and will default to ""(blank string)
          })
  • attachment: a text message may or may not have an attachment while a non-text message must have an attachement(image, audio etc).
attachment: Joi
            .object({              // attachment object must match this format
                fileName: Joi
                          .string()
                          .required(),  // must be a string and key is required
                fileUrl: Joi.string().required(),           // same as above
                ext: Joi.string().required(),                // same as above
             })

An object is used to say a key accepts an object as a value. Moreover, for fine-grain validation, we can provide validation for each of the keys of the object.

const Joi = require("joi");

// message validator, index.js will automatically pass on Joi when calling this validator

module.exports = Joi => {
  return Joi.object({
    parentId: Joi.number().default(null).allow(null),
    channelId: Joi.number()
      .required()
      .when("userId", { not: null, then: Joi.allow(null) }),
    userId: Joi.number().required(),
    type: Joi.valid("text", "image", "video", "file", "audio").default("text"),
    message: Joi.string().when("type", {
      is: "text",
      then: Joi.required(),
      otherwise: Joi.allow(""),
    }),
    attachment: Joi.object({
      fileName: Joi.string().required(),
      fileUrl: Joi.string().required(),
      ext: Joi.string().required(),
    }).when("type", {
      not: "text",
      then: Joi.required(),
      otherwise: Joi.optional(),
    }),
  }); 
};

Now that we have created our message validation we can use it as a middleware in our /message API route like below.

const { messageValidator } = require("./validation");
app.post("/message", messageValidator, (req, res) => {
  res.status(200).json(req.body);
});

todoTask.js

Consider one more scenario where we have a to-do application API and we have a /create endpoint to create a task and we want to validate the task data in the request body before saving it in our database.

Requirements

  • title:
title: Joi
       .alphanum()
       .min(3)
       .max(30)
       .regex(/^[^0-9]/)    // must not start with a number
       .required()

regex is the same as using the pattern here.

  • description: except string length it’s same as above title
  • severity: must be any one of the three less important, important, very important.
severity: Joi
          .valid("less important", "important", "very important")  // any one of
          .default("less important")    // takes "less important" as default value if key is not present

default is to define what value we want the key to be if the key is not present.

  • type: type of task
type: Joi
      .valid("single", "group")    // must be any one
      .required()
  • userIds: suppose that a task can be assigned to single user or multiple(array) userIds.
userIds: Joi
         .required()               // key must be passed
         .when("type", {           // based on type value
             is: "single",         // if type is single
             then: Joi   // then we allow userId to be either a number or string
                   .alternatives().try(
                       Joi.number(),           
                       Joi.string()
                    ),
                 otherwise: Joi
                            .array()              // must be an array
                            .items([
                                Joi.number(),
                                Joi.string()]
                            )    // whose items may be number or string
                            .min(2)               // must have aleast 2 items
                            .max(10)              //max items allowed is 10
                            .uinque()             //  must not have duplicates 
          })

An alternative is to check if our value matches any one of the alternatives provided whereas items are to define that items of the array must be a number or a string. Instead of an array, items accepts a single schema like Joi.number() too.

  • startTime: start time of a task
startTime: Joi
           .date()
           .greater('now')  // must not be a date time in the past
           .iso()      // date must be of ISO 8601 (eg 2022-03-16T14:36:30+00:00)
           .required()      // key is required
  • endTime:
endTime: Joi
         .date()
         .greater(Joi.ref('startTime'))   // must be greater than startTime
         .iso()      // date must be of ISO 8601 (eg 2022-03-16T14:36:30+00:00)
         .optional()                    // task may not have an endTime/expiry
         .allow(null)

ref is to refer to another key.

  • taskStatus: status of a task
taskStatus: Joi
            .boolean()
            .allow(null)
            .default(0)
            .required()
const Joi = require("joi");

// message validator, index.js will automatically pass on Joi when calling this validator

module.exports = Joi => {
  Joi.object({
    title: Joi.string()
      .alphanum()
      .min(3)
      .max(30)
      .regex(/^[^0-9]/)
      .required(),
    description: Joi.string()
      .min(10)
      .max(100)
      .regex(/^[^0-9]/)
      .required(),
    severity: Joi
              .valid("less important", "important", "very important")
              .default("less important"),
    type: Joi.string().valid("single", "group").required(),
    userIds: Joi
      .required()
      .when("type", {
        is: "single",
        then:  Joi
               .alternatives().try(
                  Joi.number(),           
                  Joi.string()
                ),
        is: "group",
        then: Joi.array()
          .items([Joi.number(), Joi.string()])
          .min(1)
          .max(10)
          .unique()
      }),
    startTime: Joi.date().greater("now").iso().required(),
    endTime: Joi.date().greater(Joi.ref("startTime")).iso().required(),
    taskStatus: Joi.boolean().allow(null).default(0).required()
  });
}

Now that we have created our todo validation we can use it as a middleware in our /todo API route like below.

const { todoTaskValidator } = require("./validation");
app.post("/todo", todoTaskValidator, (req, res) => {
  res.status(200).json(req.body);
});

So Joi is a very powerful library using which we can implement powerful data validation in our code. As it is not possible to demonstrate every feature in a single tutorial we have tried to go through a few.

Conclusion

Finally, we have reached the end of this tutorial and hope you guys liked it. In case you have any doubts or questions please feel free to leave a comment down below otherwise show us your love by sharing it with your friends and social media.

Leave a Reply

Back To Top