Using Pipes for Transformation and Validation in NestJS

In NestJS, pipes are a powerful and flexible mechanism for handling data transformations and validation. They allow you to manipulate data, validate inputs, and provide custom logic for incoming request data. By combining validation and transformation in pipes, you can build highly reusable and customizable data-handling logic for your application.

In this article, we’ll explore how to use NestJS pipes for both transformation and validation, two essential features that help ensure your application’s robustness and data integrity.

Table of Contents

  1. Introduction
  2. What are Pipes in NestJS?
  3. Setting Up Validation and Transformation with Pipes
  4. Creating a Custom Pipe for Validation and Transformation
  5. Using Built-in ValidationPipe
  6. Using Custom Pipes for Transformation
  7. Applying Pipes Globally and Locally
  8. Best Practices for Using Pipes
  9. Handling Validation Errors
  10. Conclusion

Introduction

In web development, data validation ensures that incoming data is correctly structured and valid. Transformation is used to convert data from one format to another, such as transforming plain JavaScript objects into class instances. In NestJS, pipes are the primary tool for both validation and transformation, making them a key part of data handling in your application.

This article will show you how to use NestJS pipes for both validation and transformation, ensuring that your incoming data is both properly formatted and valid before it is processed.

What are Pipes in NestJS?

In NestJS, pipes are classes that operate on incoming data before it reaches your controller’s route handlers. They can be used to validate, transform, or even handle errors in the data.

Pipes in NestJS can be applied globally, at the controller level, or at the individual route handler level. They are incredibly useful for creating reusable logic that can handle common operations such as data validation or transformation.

Key Concepts of Pipes:

  • Transformation: Modifying the format of incoming data. For example, converting strings to numbers, or transforming plain objects into class instances.
  • Validation: Ensuring the incoming data matches the expected format, such as verifying that a field is a valid email or that an integer is within a specific range.

Setting Up Validation and Transformation with Pipes

To use pipes for validation and transformation in NestJS, we typically use the ValidationPipe and ClassSerializerInterceptor with the class-transformer library.

Install Dependencies

If you haven’t already installed class-validator and class-transformer, you can do so by running:

bashCopyEditnpm install class-validator class-transformer

These libraries allow you to define validation rules and transform data as needed.

Creating a Custom Pipe for Validation and Transformation

In some cases, you may need to create your own custom pipes to perform specific data transformations or validations.

Custom Pipe Example

Let’s create a custom pipe that validates whether an age is above 18 and transforms the incoming data by multiplying the age by 2 before passing it to the controller:

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

@Injectable()
export class AgeValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    // Check if age is present and is a number
    if (!value.age || typeof value.age !== 'number') {
      throw new BadRequestException('Age must be a number');
    }

    // Validate age is above 18
    if (value.age < 18) {
      throw new BadRequestException('Age must be at least 18');
    }

    // Transform: Multiply age by 2
    value.age = value.age * 2;

    return value;
  }
}

Explanation:

  • Validation: We check if the age field is present and ensure it is a number. If it is missing or invalid, we throw a BadRequestException.
  • Transformation: After validating, we modify the age field by multiplying it by 2 before passing it to the controller.

You can use this custom pipe in a controller like this:

typescriptCopyEditimport { Controller, Post, Body, UsePipes } from '@nestjs/common';
import { AgeValidationPipe } from './pipes/age-validation.pipe';

@Controller('users')
export class UserController {
  @Post()
  @UsePipes(AgeValidationPipe)
  createUser(@Body() body: any) {
    return `User with age ${body.age} created successfully!`;
  }
}

Using Built-in ValidationPipe

NestJS provides a built-in ValidationPipe that combines both validation and transformation in one pipe. It automatically validates the data based on your DTO (Data Transfer Object) classes and transforms them into class instances.

Here’s how you can use the ValidationPipe:

  1. Enable Global Validation in main.ts:
typescriptCopyEditimport { ValidationPipe } from '@nestjs/common';

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

bootstrap();
  1. Define a DTO with Validation Rules:
typescriptCopyEditimport { IsString, IsInt, IsEmail, Min, Max } from 'class-validator';

export class CreateUserDto {
  @IsString()
  firstName: string;

  @IsString()
  lastName: string;

  @IsEmail()
  email: string;

  @IsInt()
  @Min(18)
  @Max(100)
  age: number;
}
  1. Use the DTO with Validation in the Controller:
typescriptCopyEditimport { Body, Controller, Post } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UserController {
  @Post()
  createUser(@Body() createUserDto: CreateUserDto) {
    return `User ${createUserDto.firstName} created successfully!`;
  }
}

In this example, the ValidationPipe automatically validates the incoming request based on the validation rules defined in the CreateUserDto.

Benefits of ValidationPipe:

  • Automatic Transformation: It transforms the incoming data into an instance of the DTO class.
  • Automatic Validation: It validates the incoming data according to the rules defined in the DTO.
  • Global Use: You can apply the ValidationPipe globally, making it easier to ensure validation throughout your app.

Using Custom Pipes for Transformation

While the ValidationPipe handles most validation needs, you may want to create custom transformation logic for specific use cases. For example, you might want to convert all incoming strings to lowercase before processing.

Here’s an example of a custom pipe that transforms an incoming string to lowercase:

typescriptCopyEditimport { Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class LowercasePipe implements PipeTransform {
  transform(value: any) {
    if (typeof value === 'string') {
      return value.toLowerCase();
    }
    return value;
  }
}

You can then use the LowercasePipe in your controller:

typescriptCopyEditimport { Controller, Post, Body, UsePipes } from '@nestjs/common';
import { LowercasePipe } from './pipes/lowercase.pipe';

@Controller('users')
export class UserController {
  @Post()
  @UsePipes(LowercasePipe)
  createUser(@Body() body: any) {
    return `User email: ${body.email} created successfully!`;
  }
}

Applying Pipes Globally and Locally

You can apply pipes globally or locally in NestJS.

Global Pipes

To apply a pipe globally, use the useGlobalPipes() method in your main.ts file:

typescriptCopyEditapp.useGlobalPipes(new ValidationPipe());

This will apply the ValidationPipe to all incoming requests, ensuring that validation and transformation happen automatically for all routes.

Local Pipes

To apply a pipe to a specific route handler, use the @UsePipes() decorator:

typescriptCopyEditimport { UsePipes } from '@nestjs/common';

@Controller('users')
export class UserController {
  @Post()
  @UsePipes(new ValidationPipe())
  createUser(@Body() createUserDto: CreateUserDto) {
    return `User ${createUserDto.firstName} created successfully!`;
  }
}

Best Practices for Using Pipes

  1. Use Built-in ValidationPipe for Standard Validation: Use the built-in ValidationPipe for most validation and transformation needs.
  2. Create Custom Pipes for Specific Needs: For custom validation or transformation logic, create your own pipes.
  3. Apply Validation Globally: To ensure consistent validation across your application, apply validation globally in the main.ts file.
  4. Keep Pipes Reusable: Make your custom pipes reusable by keeping the logic simple and focused on a single task.

Handling Validation Errors

When validation fails, the ValidationPipe will automatically throw a BadRequestException with a detailed message. For example:

jsonCopyEdit{
  "statusCode": 400,
  "message": ["age must be a number", "age must be at least 18"],
  "error": "Bad Request"
}

You can customize error handling by extending the ValidationPipe or using custom exception filters.

Conclusion

Pipes in NestJS provide a clean and efficient way to handle data validation and transformation. Whether using the built-in ValidationPipe for standard validation or creating custom pipes for complex logic, they ensure that your incoming data is well-formed and ready for processing.