Subha Chanda Subha is a web developer who is passionate about learning and experimenting with new things.

Understanding object validation with Joi in NestJS

9 min read 2618

Red Lion Head Logo Over Leafy Backround

When building a robust and scalable API, it’s important to implement proper validation within your application not only in terms of security but also to keep your database clean. If you’re working with robust backend APIs, NestJS can be a great tool to use, and, you can add object validation with NestJS and Joi to make your application secure.

This article will show you how to implement Joi object validation in NestJS via a basic CRUD example.

Jump ahead:

What is NestJS?

Heavily inspired by Angular, NestJS is an especially popular framework for building scalable and robust applications. It provides a clean architecture based on modules, controllers, and providers to help you get started. It supports TypeScript, but you can also write your applications with vanilla JavaScript.

NestJS doesn’t impose any programming paradigm on you. You are free to use object-oriented, functional, or functional reactive programming. It also provides an excellent routing mechanism under the hood and natively supports the HTTP server framework Express. You can also configure your NestJS application to use Fastify.

The building blocks of NestJS

NestJS has three main building blocks: modules, controllers, and providers.

  • Modules: modules are used to modularize the codebase and split it into reusable components. TypeScript files that are grouped are described with the @Module decorator to provide metadata. NestJS uses these files to organize the application structure
  • Controllers: controllers control the incoming requests and send the appropriate response to the client
  • Providers: providers are the fundamental concept of NestJS. Services, factories, helpers, repositories, etc., are treated as providers in Nest. Providers can be injected as a dependency in Nest

Why use NestJS?

In recent years, Node.js has become a popular programming language, and, because developers want to write production-ready APIs faster, the demand for a quality backend framework has also increased. Enter NestJS, which helps developers write efficient codes faster.

Let’s discuss the benefits of using NestJS.

  • TypeScript: NestJS uses TypeScript by default, but you can also write your NestJS code with vanilla JavaScript
  • Microservices: NestJS supports GraphQL, WebSockets, gRPC, and MQTT, along with REST APIs. The support for these tools helps to write microservices
  • CLI: Nest also has its own CLI, which enables you to create, run, build your applications, and more
  • High-quality documentation: NestJS is well documented and helps you to understand the concepts in depth. It even offers official courses created by NestJS team members
  • Testing: NestJS provides support for testing your application with Jest, or any other testing framework of your choice. NestJS even provides a testing package of its own

What is Joi?

All developers know that it’s vital to validate data coming from clients. If you have ever used MongoDB and mongoose in Node.js, you are likely familiar with mongoose schemas. Mongoose schemas help describe the data and easily add validators for the data. Joi is very similar to schemas.

Joi is a widely used Node.js data validation library that provides a simple, intuitive, and readable API to describe data. It’s primarily used to validate data sent from API endpoints and allows you to create blueprints of the data type you want to accept.

Here is a simple example of describing schema with Joi:



 const schema = Joi.object().keys({ 
     name: Joi.string().alphanum().min(3).max(30).required(),
     birthyear: Joi.number().integer().min(1970).max(2013), 
});

Building a basic API with NestJS

NestJS provides many options to scaffold code according to your needs, such as the CRUD recipe. This allows you to scaffold a CRUD with endpoints within a few seconds from the Nest CLI.

To install the Nest CLI on your computer, run the following command:

npm i -g @nestjs/cli  

The next step is to generate a Nest application. The Nest CLI uses the following command:

nest new project-name

Here, project-name is the name of the project. After the command completes, run the following command to scaffold the CRUD endpoints:

nest g resource users

It’ll ask you a few questions, such as which transport layer to use. Once you choose the options according to your preference, Nest will scaffold the CRUD API. For example, an API with the users endpoint will be generated from the above command. You can see the new users folder.

Now, if you run the application with npm run start:dev, you’ll see the endpoints logged in the console. Your server will be started at port 3000.

You can either check the endpoints by visiting them or opening the users.controllers.ts file. This file contains the routes for the CRUD API. The services for each API are defined in the users.service.ts file, and all these files are under the users folder.


More great articles from LogRocket:


The importance of object validation

If you look at the GET method for finding a single item in the users.controllers.ts file, you’ll find that there is no validation set up. You can use anything as an ID, and Nest will not throw a validation error.

The OWASP top ten list of security risks mentions that injection attack is still one of the most popular security risks. OWASP also mentions that an application is vulnerable to injection attacks when “user-supplied data is not validated, filtered, or sanitized by the application.”

This clearly shows that data validation is an important security concern to keep in mind when building applications. There are built-in pipes that can verify or modify the input. NestJS has eight built-in pipes. If you want the ID to be only of integer type, you can use the ParseIntPipe pipe. Here’s an example:

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: string) {
  return this.usersService.findOne(+id);
}

If you try to hit the endpoint with any ID other than a numeric value, you’ll receive the following error.

Built-in Pipes Validation with NextJS

Using a inbuilt pipe is simple, but using it for a large schema is complicated. Joi makes it easier to design schemas and implement validation in NestJS. Let’s implement Joi for the NestJS project.

Implementing Joi in NestJS

Generally, any script that can be injected into NestJS is the Pipe class. Pipes primarily have two use cases:

  • Transforming input data
  • Validating input data

You can read more about pipes in the official documentation.

The first step is to install the necessary packages. Here, only the Joi package is required. Run the following command to install the package.

npm i joi

Now, create a new file called validation.pipe.ts inside the users directory. Creating a custom pipe to implement validation is pretty straightforward. Here’s a code snippet to help you understand.

import {
  PipeTransform,
  BadRequestException,
    ArgumentMetadata
} from '@nestjs/common';

export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}

Any schema passed into this pipe constructor will be checked for the configured Joi validation schema. To make the above validator work, open the create-user.dto.ts file inside the dto folder.

Here, define a schema type that the API will use when saving the data. For simplicity, assume that the schema sent by the user and held by the database have the same structure.

Let’s assume the API takes firstname, lastname, email, isVerified, and phoneNumber as input. The DTO will look like this:

export class CreateUserDto {
  public firstname: string;
  public lastname: string;
  public isVerified: boolean;
  public email: string;
  public phoneNumber: number;
}

Now, define the Joi schema inside the user.dto.js file. You can also use separate files to store the schemas. The Joi user schema is simple for this example.

import Joi from 'joi';

export const UserSchema = Joi.object({
  firstname: Joi.string().required(),
  lastname: Joi.string().required(),
  email: Joi.string().email().required(),
  isVerified: Joi.boolean().required(),
  phoneNumber: Joi.number(),
}).options({
  abortEarly: false,
});

The schema is pretty self-explanatory. The string() method ensures that the input is of type string, and the required() method makes certain the fields are inside the input. Similarly, boolean and number make sure the types are boolean or number.

The options method takes other options inside an object. The abortEarly method, when set to true, stops the validation when it finds the first error. Otherwise, it returns all the errors.

Now that the schemas are ready, it is time to update the validation pipe accordingly.
Here is the complete validation.pipe.ts file.

import { PipeTransform, BadRequestException } from '@nestjs/common';

import { CreateUserDto } from './dto/create-user.dto';

import { UserSchema } from './dto/user.dto';

export class CreateUserValidatorPipe implements PipeTransform<CreateUserDto> {
  public transform(value: CreateUserDto): CreateUserDto {
    const result = UserSchema.validate(value);
    if (result.error) {
      const errorMessages = result.error.details.map((d) => d.message).join();
      throw new BadRequestException(errorMessages);
    }
    return value;
  }
}

The custom validator class accepts and returns the CreateUserDto class. The const result = UserSchema.validate(value); validates the result according to the defined Joi schema. If the result has any errors, the results are mapped using the map method. The error messages are joined together. Finally, the error messages are sent to the client. Otherwise, it returns the input value.

If the input passes the validation, it’ll show the message, “This action adds a new user,” according to the method defined inside the user.service.ts file.

We’ve now implemented Joi into NestJS. You can see if the validation is working by sending the JSON payload to the endpoint http://localhost:3000/users.

Validating different data types with Joi

Now that we have seen some prior validation types, let’s explore some more types. You’ll see validating dates, arrays, and strings with Joi and NestJS.

Validating the date with Joi

Validating the date with Joi is also straightforward. The date() validator checks if the passed data is of type date.

Under the hood, the date validator uses the JavaScript Date.parse function to convert a few invalid dates to valid dates. For example, if the user passes 2/31/2029, it will be converted to 3/3/2029.

The date validator has a few methods like greater, less, and iso. The greater function can check if a date is greater than the specified date. Similarly, the less function will check if the value is less than the given date.

Let’s make a validation field for the date of birth. In the user.dto.ts file, add the following line below phoneNumber:

dob: Joi.date().less('1-12-2022')

Here, the validator will check if the date is less than 1 December 2022. If you try to pass a date greater than the given, it’ll return an error.
The date validator also has an iso method that checks if the passed date is of valid ISO 8601 format. You can learn more about the date function from the official documentation.

Validating an array with Joi

Joi with NestJS also provides validators for arrays. There are multiple methods available with the array validator. For example, you can use the length function with the array validator to check if the array is of the specified length. The schema would be:

arr: Joi.array().length(5)

The above object will only accept an array that is of length 5. Using the max and min function, you can also set a minimum or a maximum number of acceptable array elements.

You can also validate an array of objects using the array validator. Let’s look at the validator object to understand how it can be done.

items: Joi.array().has(
    Joi.object({
      name: Joi.string().valid('item1', 'item2', 'item3').required(),
      price: Joi.number().required(),
    }),
  ),

Here, the item object will hold an array of objects. The object can have two fields: name and price. The name field can only contain values item1, item2, or item3. The valid method holds the valid values for the specific field. And the price field can only be of numeric type.

The request for items must be similar to the following. Otherwise, it’ll throw an error:

 "items": [
    {
      "name": "item1",
      "price": 4
    },
    {
      "name": "item1",
      "price": 5
    }
  ]

There are many other functions available on the array validator. You can check all of the functions here.

Validating strings with Joi

Joi also contains multiple functions for validating different types of strings. You cannot pass an empty string by default, but this rule can be overwritten using allow('') method. You can also set up default values using the default() method.

Multiple methods are available on strings, like alphanum, base64, creditCard, ip, etc. The creditCard validator method uses the Luhn Algorithm for validating credit card numbers. The usage is similar to the above methods. You simply chain the methods with the validator function. Here’s an example:

creditCard: Joi.string().creditCard().required(),

The above-discussed data types are only a few that Joi offers. Many other types of data can be easily validated using Joi and NestJS. You can refer to the official documentation to expand your validation schema.

Testing the schema with Thunder Client

Thunder Client is a VS Code extension used for quick API testing. It is similar to Postman or Insomnia, but it is lightweight and has fewer functionalities than Postman.

Thunder Client is enough for testing because the API built in this application only consists of essential features. You’ll find Thunder Client very similar if you are familiar with Postman or any other REST client.

By default, it contains two panels. The left panel specifies the request, type, data, headers, contents, etc. The right column shows the status of the request, the response, and other response-related information.

The API route for this application is localhost:3000/users. You can hit the route with GET or POST methods to check if it’s working. But before hitting the routes, make sure your application is running. You can run the application by running the following command in the terminal:

npm run start:dev

Now, on the left side panel of Thunder Client, set the method as post and change the default URL to http://localhost:3000/users. Hit Send, and the right panel will show the result shown below.

Thunder Client Example

Now, let’s try to send a POST request to the same endpoint. The POST request should contain a JSON body. The JSON body should include the following fields:

  • firstname
  • lastname
  • email
  • isVerified
  • phoneNumber
  • dob
  • creditCard
  • items

Let’s take a look at a request example:

{
  "firstname": "Subha",
  "lastname": "Chanda",
  "email": "[email protected]",
  "isVerified": true,
  "phoneNumber": 7777777777,
  "dob": "1-1-2022",
  "creditCard": "4242424242424242",
  "items": [
    {
      "name": "item1",
      "price": 4
    },
    {
      "name": "item1",
      "price": 5
    }
  ]
}

The above JSON request is valid. Try pasting this request to the JSON Content field in the left panel of Thunder Client. Also, change the request type to POST. Send the request, and you’ll receive the output as shown below:

Testing Joi Validation with Thunder Client

The right panel would show the text This action adds a new user, and the JSON body will be logged to the console.

Let’s change the credit card data to invalid data. Removing three digits from the credit card will make it invalid. If you send the request after removing three numbers, you’ll receive the error stating "\"creditCard\" must be a credit card".

Similar to this, you can test the other data types. A JSON object will be returned for invalid data, and the message key in the object will consist of the error.

Conclusion

This article provided an overview of NestJS and Joi and the importance of validation in our apps, then walked you through implementing validation within a NestJS application. I hope you found it useful.

Remember: it is essential to implement proper validation methods to build a robust application. You can check out the Joi and NestJS documentation to better understand the library and frameworks.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Subha Chanda Subha is a web developer who is passionate about learning and experimenting with new things.

Leave a Reply