Introduction

Class validation in NestJS is used for ensuring data integrity in your applications. By leveraging powerful decorators and validation rules, NestJS allows you to effortlessly validate incoming data, preventing errors before they happen. In this blog, we’ll explore how to do the class validation in NestJS to get clean and reliable inputs for every request.

Prerequisites

In order to implement class validation, a few packages must be installed. To install them, use the following command.

npm install class-validator class-transformer

After the packages have been installed, we need to modify the pipeline to use class validation by adding the validation pipe. It is done in the main.ts file of a NestJS application. The code that must be added looks like this:

app.useGlobalPipes(new ValidationPipe({
  whitelist: true,
  // forbidNonWhitelisted: true,
  transform: true,
}));

Although many properties and configurations can be passed to a validation pipe, there are three that are most commonly used. The first one is the whitelist: true that we will use in all examples provided. It just strips all additional properties that are sent in the request and are not defined in the DTO. If the app you are building for some reason requires the incoming request properties to be only the ones specified in the DTO and you want to remove the possibility of sending requests with additional properties, you can instead use the forbidNonWhitelisted: true property. This will throw an error when the request reaches your app. The third most commonly used property is the transform: true property. This one automatically transforms incoming data to the right type. For example, if the DTO specified in your app expects a number and the incoming request provides a string for that property, with a correctly set class transformation, it will automatically be converted to a number. In cases where it is not possible to transform property types, the application will throw an error.

Getting Started: A Basic Validation Example

After the successful setup, it is time to build the first example. Let’s say you want to create a new user for your app, and you are sending the request with user data to your NestJS app. First, we will make a DTO class with some basic information you might need to create a user.

export class CreateUserRequestBodyDto {
  email: string;
  password: string;
  age: number;
  name?: string;
  newsletterSubscribed: boolean;
}

Now let’s finally use class validation and implement validations for this class. Validation here works by adding the necessary decorator above the properties. The first set of decorators to consider are decorators that determine whether a property is required or not. In the class above, it can be noticed that all properties are mandatory, except the name. So the request we want to get consists of all mandatory properties and additionally the name property if the user has provided his name. The class then looks like this:

import { IsNotEmpty, IsOptional } from "class-validator";

export class CreateUserRequestBodyDto {
  @IsNotEmpty()
  email: string;

  @IsNotEmpty()
  password: string;

  @IsNotEmpty()
  age: number;

  @IsOptional()
  @IsNotEmpty()
  name?: string;

  @IsNotEmpty()
  newsletterSubscribed: boolean;
}

We used two decorators for this purpose:

  • @IsNotEmpty() - ensures that a value is provided for the property and that value is not null, undefined or an empty string for example
  • @IsOptional() - provides the possibility for a property to be omitted from the request, but if it is sent, the value provided is validated against all other constraints.

So now, if the server receives a request that does not have all of the properties required, for example, the email and the age, the following error will be thrown:

{
    "message": [
        "email should not be empty",
        "age should not be empty",
    ],
    "error": "Bad Request",
    "statusCode": 400
}

After ensuring that all of the values are provided, now let’s ensure that the values provided are of the right type. The class now looks like this:

import { IsNotEmpty, IsOptional, IsEmail, IsInt, IsString, IsBoolean } from "class-validator";

export class CreateUserRequestBodyDto {
  @IsNotEmpty()
  @IsEmail()
  email: string;

  @IsNotEmpty()
  @IsString()
  password: string;

  @IsNotEmpty()
  @IsInt()
  age: number;

  @IsOptional()
  @IsNotEmpty()
  @IsString()
  name?: string;

  @IsNotEmpty()
  @IsBoolean()
  newsletterSubscribed: boolean;
}

We used a few decorators here:

  • IsEmail - used for email validation, validates if the value provided follows the standard email structure
  • IsString - checks if the provided value is a string
  • IsInt - checks if the provided value is an integer number
  • IsBoolean - checks if the provided value is a boolean

In case the wrong type is sent, one of the following messages with the error would be thrown:

{
    "message": [
        "email must be an email",
        "password must be a string",
        "age must be an integer",
        "newsletterSubscribed must be a boolean",
    ],
    "error": "Bad Request",
    "statusCode": 400
}

To complete the class validation process, some additional constraints should be added regarding the range of possible integers provided, length of the strings, and similar. The final version of the class after class validation is added looks like this:

import { IsNotEmpty, IsOptional, IsEmail, IsInt, IsString, IsBoolean, MinLength, MaxLength, Min } from "class-validator";

export class CreateUserRequestBodyDto {
  @IsNotEmpty()
  @IsEmail()
  email: string;

  @IsNotEmpty()
  @IsString()
  @MinLength(8)
  password: string;

  @IsNotEmpty()
  @IsInt()
  @Min(18)
  age: number;

  @IsOptional()
  @IsNotEmpty()
  @IsString()
  @MaxLength(40)
  name?: string;

  @IsNotEmpty()
  @IsBoolean()
  newsletterSubscribed: boolean;
}

Decorators used for this purpose are:

  • MinLength & MaxLength - used to determine the minimal or maximal length of a string
  • Min & Max - used to determine the minimal or maximal value of a number

And now the validation for the User DTO class is finished. After we have finished implementing the validation, let’s also add data transformations. It will be used to transform incoming request properties to the type we need, and at the same time the data can be sanitized.

Let’s see how it is implemented:

import { IsNotEmpty, IsOptional, IsEmail, IsInt, IsString, IsBoolean, MinLength, MaxLength, Min } from "class-validator";
import { Transform, Type } from 'class-transformer';

export class CreateUserRequestBodyDto {
  @IsNotEmpty()
  @IsEmail()
  @Transform(({ value }) => value.toLowerCase().trim())
  email: string;

  @IsNotEmpty()
  @IsString()
  @MinLength(8)
  @Transform(({ value }) => value.trim())
  password: string;

  @IsNotEmpty()
  @IsInt()
  @Min(18)
  @Type(() => Number)
  age: number;

  @IsOptional()
  @IsNotEmpty()
  @IsString()
  @MaxLength(40)
  @Transform(({ value }) => value.trim())
  name?: string;

  @IsNotEmpty()
  @IsBoolean()
  newsletterSubscribed: boolean;
}

To perform data transformation and sanitization, we used two decorators from class-transformer and those are:

  • Transform - defines a custom logic for value transformation, here it is used for handling string values that are trimmed and, in the email case, converted to lowercase
  • Type - specifies to what type of class the incoming data property should be converted to

And we are finally done. This is our DTO for a new User and it can now be used.

Advanced examples

Arrays

For example, if we want to send an array of users to the server where every user has an email and a name, what is the proper way to validate that array? The answer is right below.

import { IsNotEmpty, IsOptional, IsEmail, IsString } from "class-validator";
import { Transform, Type } from 'class-transformer';

export class UserDto {
  @IsNotEmpty()
  @IsEmail()
  @Transform(({ value }) => value.toLowerCase().trim())
  email: string;

  @IsNotEmpty()
  @IsString()
  @Transform(({ value }) => value.trim())
  name: string;
}

export class UserListDto {
  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => UserDto)
  users: UserDto[];
}

To validate every object inside an array, the @ValidateNested() decorator must be used with { each: true } property in order to validate every element in the array individually. If we were to leave that property’s default value, which is false, the validation would fail since NestJS would try to validate the entire array as a single instance of that DTO.

Custom validation

In case you want to validate data against some custom business rule that you can not do using the available decorators, you can write your own. In the example with users and their emails, let’s say you have a constraint that all of the emails in that array must be unique, and you want to validate that immediately when the server receives that request. This is how you can do it:

import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';

@ValidatorConstraint({ name: 'uniqueEmailArray', async: false })
export class UniqueUsersEmailArray implements ValidatorConstraintInterface {
  validate(users: UserDto[], args: ValidationArguments) {
    const emails = users.map(user => user.email);
    return new Set(emails).size === emails.length;
  }

  defaultMessage(args: ValidationArguments) {
    return 'All user emails must be unique';
  }
}

To create your own custom validation class, you must mark it with the @ValidatorConstraint decorator. You have to specify if it is async or not, and you can specify the constraint name or it will be auto-generated. The class must implement ValidatorConstraintInterface and its validate method that return a boolean value depending if the validation was successful. Here, we validated the array by making it a set, which implies that all duplicates from the original array are removed. If the length stays the same, it means that there are no duplicates and the validation is successful. To use that custom validation class, all you need to do is add @Validate() decorator with the name of your validation class as an argument.

import { IsNotEmpty, IsOptional, IsEmail, IsString } from "class-validator";
import { Transform, Type } from 'class-transformer';

export class UserListDto {
  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => UserDto)
  @Validate(UniqueUsersEmailArray)
  users: UserDto[];
}

Conclusion

Incorporating class validation into your NestJS applications is a crucial step in ensuring data integrity and security. By leveraging the powerful tools provided by class-validator and class-transformer packages, you can efficiently handle validation by following the provided examples.