Skip to main content

Pipes and Validators in NestJS

Pipes are core architectural components in the request lifecycle of NestJS. They operate on incoming method arguments before the route handler is invoked, fulfilling two critical purposes: data transformation and validation.

To follow the practical section of this guide, you can base your project on the course repository on GitHub: Kelocoes/compunet3-20252 on the nest/intro branch.


1. Concepts and Architectural Foundations

What is a Pipe in NestJS?

A Pipe is a class annotated with @Injectable() that implements the PipeTransform interface. Unlike standard Express middlewares, pipes run within the NestJS execution context immediately prior to controller method invocation. This gives them access to argument metadata (ArgumentMetadata), enabling them to:

  1. Transformation: Mutate input data to the required format or type (e.g., parsing a URL string "42" into an integer 42, or instantiating a domain entity).
  2. Validation: Assess whether incoming data complies with schema restrictions and business validation rules. If the data is valid, execution proceeds; if invalid, the pipe halts request handling by throwing an HTTP exception (typically BadRequestException 400).

Anatomy of the PipeTransform Interface

Every custom pipe must implement the transform(value: any, metadata: ArgumentMetadata) method:

  • value: The raw incoming parameter value currently being processed (originating from @Body(), @Param(), or @Query()).
  • metadata: Metadata describing the method argument:
    • type: Indicates parameter origin ('body', 'query', 'param', or 'custom').
    • metatype: The expected TypeScript type/class (e.g., CreateUserDto or Number).
    • data: The string passed to the parameter decorator (e.g., @Param('id') yields data = 'id').

Built-in Pipes in NestJS

NestJS ships with ready-to-use pipes exported from @nestjs/common:

Built-in PipePurposeExample Usage
ValidationPipeValidates DTO schemas using class-validator and transforms payloads via class-transformer.Global or on @Body()
ParseIntPipeParses string values to integer primitives (number). Throws 400 if invalid.@Param('id', ParseIntPipe)
ParseFloatPipeParses string values to floating-point numbers.@Query('price', ParseFloatPipe)
ParseBoolPipeConverts "true", "false", true, false into boolean values.@Query('active', ParseBoolPipe)
ParseArrayPipeParses comma-separated lists into typed arrays.@Query('ids', new ParseArrayPipe(...))
ParseUUIDPipeValidates that a string is a valid UUID (v3, v4, or v5).@Param('uuid', new ParseUUIDPipe())
ParseEnumPipeVerifies that the input value matches an allowed TypeScript enum.@Param('status', new ParseEnumPipe(CatStatus))
DefaultValuePipeSupplies a fallback value when the client omits an optional query parameter.@Query('page', new DefaultValuePipe(1))
ParseFilePipeValidates uploaded files (size, mime-type) when using @UploadedFile().Multipart file uploads

Using Built-in Pipes on Route Parameters

src/cats/cats.controller.ts
import {
Controller,
Get,
Param,
Query,
ParseIntPipe,
ParseUUIDPipe,
ParseEnumPipe,
DefaultValuePipe,
} from '@nestjs/common';

export enum CatBreed {
SIAMESE = 'siamese',
PERSIAN = 'persian',
MAINE_COON = 'maine_coon',
}

@Controller('cats')
export class CatsController {
// Automatically transforms the ID param to a number
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return { id, type: typeof id }; // id is guaranteed to be a number
}

// Enforces valid UUID v4 format
@Get('by-uuid/:uuid')
findByUuid(@Param('uuid', new ParseUUIDPipe({ version: '4' })) uuid: string) {
return { uuid };
}

// Validates against enum values and sets fallback pagination
@Get('filter/:breed')
filter(
@Param('breed', new ParseEnumPipe(CatBreed)) breed: CatBreed,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
) {
return { breed, page };
}
}

Pipe Scopes

Pipes can be scoped at four distinct levels based on design requirements:

  1. Parameter Level: @Param('id', ParseIntPipe) id: number.
  2. Method Level: @UsePipes(new ValidationPipe()) above a @Post() handler.
  3. Controller Level: @UsePipes(new ValidationPipe()) above a @Controller('users') class.
  4. Global Level: Applies across every route in the entire application. Registered in main.ts using app.useGlobalPipes(new ValidationPipe()) or through module dependency injection:
src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);

// Global pipe to validate all incoming DTO payloads
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Strips properties not declared in the DTO
forbidNonWhitelisted: true, // Rejects requests containing unrecognized properties
transform: true, // Automatically converts incoming payloads into DTO instances
}),
);

await app.listen(3000);
}
bootstrap();

2. Practical Guide: Validation with class-validator & Custom Pipes

In this section, we will build a comprehensive validation system using class-validator and class-transformer, accompanied by a custom Pipe that validates positive integers.

1

Install Validation Dependencies

Install the official validation libraries needed for decorator-based validation and object transformation in NestJS:

Terminal
npm install class-validator class-transformer
2

Create a Custom Pipe: PositiveIntPipe

We build a custom pipe ensuring route parameter numbers are strictly greater than zero:

src/common/pipes/positive-int.pipe.ts
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from '@nestjs/common';

/**
* Parses string input into an integer and verifies that it is strictly positive (> 0).
*/
@Injectable()
export class PositiveIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);

if (isNaN(val)) {
throw new BadRequestException(
`The parameter '${metadata.data ?? 'id'}' must be a valid integer.`,
);
}

if (val <= 0) {
throw new BadRequestException(
`The parameter '${metadata.data ?? 'id'}' must be a positive integer greater than 0.`,
);
}

return val;
}
}
3

Define Validation Rules in the DTO

Decorate DTO fields with semantic rules provided by class-validator:

src/users/dto/create-user.dto.ts
import {
IsString,
IsEmail,
IsNotEmpty,
MinLength,
MaxLength,
IsOptional,
Matches,
} from 'class-validator';

export class CreateUserDto {
@IsString({ message: 'Username must be a string' })
@IsNotEmpty({ message: 'Username is required' })
@MinLength(3, { message: 'Username must be at least 3 characters long' })
username: string;

@IsEmail({}, { message: 'Must provide a valid email address' })
@IsNotEmpty({ message: 'Email address is required' })
email: string;

@IsString()
@MinLength(8, { message: 'Password must be at least 8 characters long' })
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).*$/, {
message: 'Password must contain at least one uppercase letter, one lowercase letter, and one number',
})
password: string;

@IsOptional()
@IsString()
@MaxLength(200, { message: 'Bio cannot exceed 200 characters' })
bio?: string;

@IsString({ message: 'Role must be a string' })
@IsNotEmpty({ message: 'Role name is required' })
roleName: string;
}
4

Connect the DTO and Custom Pipe in the Controller

Apply validation in the controller. ValidationPipe validates the request body while PositiveIntPipe validates :id:

src/users/users.controller.ts
import {
Controller,
Get,
Post,
Body,
Param,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { PositiveIntPipe } from '../common/pipes/positive-int.pipe';

@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}

/**
* POST /users -> Automatically validates request body with CreateUserDto
*/
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}

/**
* GET /users/:id -> Validates that :id is a positive integer > 0
*/
@Get(':id')
@HttpCode(HttpStatus.OK)
findOne(@Param('id', PositiveIntPipe) id: number) {
return this.usersService.findOne(id);
}
}
What happens if input validation fails?

If a client issues POST /users with an invalid email address ("email": "not-an-email"), ValidationPipe interrupts execution before calling create() and responds with:

{
"message": [
"Must provide a valid email address"
],
"error": "Bad Request",
"statusCode": 400
}

Self-Assessment Quiz

Cargando cuestionario...