Video

Watch the full video on YouTube:

Custom Pipes

Introduction

In this sixth installment of the NestJS guide, we’ll explore the internal workings of Pipes (PipeTransform), the fundamental tool in NestJS for validating and transforming input data.

What is a Pipe?

A Pipe is a class that implements the PipeTransform interface. It has a single method called transform() that receives two parameters:

  1. value - The input data
  2. metadata - Information about the argument being processed

PipeTransform Interface

interface PipeTransform<T = any, R = any> {
  transform(value: T, metadata: ArgumentMetadata): R;
}

Understanding the Transform Method

The transform() method receives:

  1. value - The actual data being processed
  2. metadata - An object containing:
    • type - The type of argument (body, query, param, etc.)
    • metatype - The class type of the argument
    • data - The name of the parameter

Example Metadata

{
  type: 'body',
  metatype: SavePetBodyDto,
  data: undefined
}

Creating a Custom Pipe

Let’s create a custom pipe for validation:

@Injectable()
export class ValidateMinPipe implements PipeTransform {
  transform(value: unknown, metadata: ArgumentMetadata) {
    if (typeof value !== 'number') {
      throw new BadRequestException('Value must be a number');
    }
    // value is now narrowed to number
    return value;
  }
}

Throwing Exceptions

When validation fails, you should throw an exception:

throw new UnprocessableEntityException('Validation failed');

This will return a 422 status code to the client.

Pipe Based on Classes

The most common way to create pipes is using classes:

@Injectable()
export class ValidateMinPipe implements PipeTransform {
  constructor(private minValue: number) {}

  transform(value: unknown, metadata: ArgumentMetadata) {
    if (typeof value !== 'number') {
      throw new BadRequestException('Value must be a number');
    }
    if (value < this.minValue) {
      throw new UnprocessableEntityException(
        `Value must be at least ${this.minValue}`
      );
    }
    return value;
  }
}

Lightweight Pipes

You can also create lightweight pipes using object literals:

const validateMinPipe = {
  transform: (value: unknown, metadata: ArgumentMetadata) => {
    if (typeof value !== 'number') {
      throw new BadRequestException('Value must be a number');
    }
    if (value < 10) {
      throw new UnprocessableEntityException('Value must be at least 10');
    }
    return value;
  }
};

Using Pipes

You can use pipes in several ways:

At Parameter Level

@Get('pets/:id')
getPet(@Param('id', ValidateMinPipe) id: number) {
  return `Pet ID: ${id}`;
}

At Body Level

@Post('pets')
createPet(@Body(ValidateMinPipe) body: unknown) {
  return body;
}

Global Level

app.useGlobalPipes(new ValidationPipe());

Transforming Data

Pipes can also transform data:

@Injectable()
export class ParseIntPipe implements PipeTransform {
  transform(value: string, metadata: ArgumentMetadata) {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed');
    }
    return val;
  }
}

Best Practices

  1. Keep pipes focused on a single responsibility
  2. Use the appropriate exception for validation errors
  3. Always return a value or throw an exception
  4. Use class-based pipes for reusability
  5. Consider using built-in pipes before creating custom ones

Conclusion

Custom pipes give you full control over data validation and transformation in NestJS. They are essential for building robust APIs.

Repository

💻 Repository: https://github.com/radagva/guia-de-nestjs-example

Next Steps

🔔 Subscribe to the channel to not miss the next chapters of the NestJS guide!

NestJS API Development Guide 05: Controllers, GET Requests, Params, Queries and PipesTypeScript Guide 01: Introduction to TypeScript