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. To ensure that our API works as intended the input data must conform to a specific 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 operations on it say saving the data to a database or authenticating a user.

Agenda

In this tutorial, we will be having a look at how we can implement a robust and powerful input validation for our Rest API in nodejs. 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 the purposes of our tutorial, we will be implementing 3 validations starting from basic validation to advance one.

  • 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. Within our 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 is just a kind of a utils file that auto imports all our Joi validations so that we don’t have to do them manually as we are lazy. As a bonus, it also handles Joi-related errors. You can use it as it is and not bother much about it as not strictly related to our API validations.

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

As someone has rightly said that the best way to learn anything is by getting your hands dirty, following that we will walk you through 3 Joi validation example use-case scenarios. Moreover, we will have a separate file for each of our validation just for the sake of separation of concerns.

registration.js

Let’s suppose we have a basic user signup API route for which we have the below requirements.

Requirements

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

    required is to say that the key must be present.

    pattern is used 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(
        // we have just copy pasted from the internet a basic working password regex for demonstration purpose
        new RegExp(
          "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[[email protected]#$%^&*])(?=.{8,})"
        )
      )
      .required(),
    repeatPassword: Joi.ref("password"),
  });
}

Now that we have created our registration validator 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 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 the messageId of a message’s parent message + not every message will have a parentId(or to say not every message will be a direct reply to another message).

    default is to define the default value of a key if not available while allow is to say that this key accepts “null” as a value too if passed.
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
  • 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).

    when” is used to apply conditional Joi validation schema based on key name, reference or a Joi schema.
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.
  • type: we only want to allow our message type to be one of the 5 values namely text, image, video, file, and audio.

    valid is to define which values we accept as valid values.
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
  • message: read code comments.
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 attachment (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 assert that 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 as shown above.

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 for string length, it’s the same as the title.
  • severity: must be any of the three less important, important, or very important.

    default is to define what value we want the key to be if the key is absent.
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
  • type: type of task
type: Joi
      .valid("single", "group")    // must be any one
      .required()
  • userIds: suppose that a task can be assigned to a 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 passes any one of the provided alternative validations. It’s like an OR clause.

One more inbuilt validator we have in Joi is items, which is used to validate an array of items and accept either an array or another Joi validation schema like Joi.number(), Joi.string() etc. In this case, we are asserting that the element at the 0th and 1st index must be a number and a string respectively.

  • 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: ref is to refer to another key’s value.
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)
  • 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);
});

In this tutorial, although we went through only 3 examples we have barely scratched the surface of what Joi is capable of as a validation library using which we can implement powerful data validation in our code like a breeze.

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 on social media.

Leave a Reply

Back To Top