Using ValidationPipe() in NestJS

Updated: January 1, 2024 By: Guest Contributor Post a comment

Introduction

Ensuring incoming data conforms to expected shapes and types is crucial for robust web applications. NestJS simplifies this process with ValidationPipe, a powerful tool to seamlessly validate input data in your server-side applications.

Setting Up

Before diving into the use of ValidationPipe, ensure that you have a NestJS project set up. Install the class-validator and class-transformer packages, which ValidationPipe leverages for validating incoming data:

npm install class-validator class-transformer

Basic Usage of ValidationPipe

Let’s start by using ValidationPipe in its simplest form:

import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UsersController {
  @Post()
  @UsePipes(new ValidationPipe())
  async createUser(@Body() createUserDto: CreateUserDto) {
    return 'User created!';
  }
}

This code demonstrates setting up a simple POST endpoint using ValidationPipe to validate the body of the incoming request against the CreateUserDto structure.

DTO and Validations

In the above example, CreateUserDto can implement various validation rules using decorators from the class-validator package. Here is a basic example of how to define such DTO (Data Transfer Object):

import { IsString, IsInt, MinLength, MaxLength, IsEmail } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(4)
  @MaxLength(20)
  username: string;

  @IsEmail()
  address: string;

  @IsInt()
  age: number;
}

Each decorator specifies a validation rule that the respective property must conform to.

Global ValidationPipe Setup

To avoid having to use the @UsePipes() decorator with every controller method, you can set up ValidationPipe globally:

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

With this setup, ValidationPipe will apply to all incoming requests across the application.

Advanced Options

ValidationPipe comes with several customizable options. For example, you can instruct it to automatically strip out any properties not part of the DTO:

app.useGlobalPipes(new ValidationPipe({ whitelist: true }));

The whitelist option will ensure only the validated properties remain in the object that is passed to your route handlers.

Another powerful option is transform which automatically transforms payloads to the DTO class instances:

app.useGlobalPipes(new ValidationPipe({ transform: true }));

This not only validates but also converts plain JavaScript objects into instances of their respective classes. Utilizing this feature is highly advantageous when working with class methods or instance-specific logic.

Group Validation and Partial Validation

There are scenarios where you may want to apply different sets of validation rules depending on the operation. NestedJS’s ValidationPipe allows for such conditionality with the use of validation groups:

import { IsString, IsInt, ValidateIf } from 'class-validator';

export class CreateUserDto {
  @IsString({ groups: ['create'] })
  username: string;

  @IsInt({ groups: ['create', 'update'] })
  @ValidateIf(o => o.age !== null, { groups: ['update'] })
  age?: number;
}

In this case, validation rules apply differently, based on the group specified when calling ValidationPipe:

app.useGlobalPipes(new ValidationPipe({ groups: ['create'] }));

This approach is particularly useful in applications with complex validation logic, like partial updates in RESTful APIs (PATCH requests).

Custom Validation Classes

You can extend the functionality of class-validator by creating custom validation classes. Here’s an example:

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

@ValidatorConstraint({ name: 'customTextLength', async: false })
export class CustomTextLengthValidator implements ValidatorConstraintInterface {
  validate(text: string, args: ValidationArguments) {
    return text.length > 10 && text.length < 20;
  }

  defaultMessage(args: ValidationArguments) {
    return 'Text must be longer than 10 and shorter than 20 characters';
  }
}

To use the custom validator, decorate your DTO properties accordingly:

import { Validate } from 'class-validator';
import { CustomTextLengthValidator } from './custom-text-length-validator';

export class CreateUserDto {
  @Validate(CustomTextLengthValidator)
  username: string;
}

This setup enables you to enforce complex or custom validations without cluttering DTOs with detailed validation specifications.

By default, when ValidationPipe detects validation errors, it throws an exception which can be handled by NestJS’s built-in exception filters or custom filters for more granular error responses.

Performance

While using ValidationPipe ensures robustness in the data processing pipeline, overusing custom validators or performing overly complex validations can impact performance. Always consider optimizing your validators for the best balance between validation thoroughness and server performance.

Conclusion

Incorporating ValidationPipe into NestJS applications brings structure and security to data handling, creating a foundation for well-architected applications. As we explored various features from basic to advanced, remember that the ultimate goal of validation is to ensure data integrity while maintaining application performance.