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:
- Transformation: Mutate input data to the required format or type (e.g., parsing a URL string
"42"into an integer42, or instantiating a domain entity). - 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
BadRequestException400).
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.,CreateUserDtoorNumber).data: The string passed to the parameter decorator (e.g.,@Param('id')yieldsdata = 'id').
Built-in Pipes in NestJS
NestJS ships with ready-to-use pipes exported from @nestjs/common:
| Built-in Pipe | Purpose | Example Usage |
|---|---|---|
ValidationPipe | Validates DTO schemas using class-validator and transforms payloads via class-transformer. | Global or on @Body() |
ParseIntPipe | Parses string values to integer primitives (number). Throws 400 if invalid. | @Param('id', ParseIntPipe) |
ParseFloatPipe | Parses string values to floating-point numbers. | @Query('price', ParseFloatPipe) |
ParseBoolPipe | Converts "true", "false", true, false into boolean values. | @Query('active', ParseBoolPipe) |
ParseArrayPipe | Parses comma-separated lists into typed arrays. | @Query('ids', new ParseArrayPipe(...)) |
ParseUUIDPipe | Validates that a string is a valid UUID (v3, v4, or v5). | @Param('uuid', new ParseUUIDPipe()) |
ParseEnumPipe | Verifies that the input value matches an allowed TypeScript enum. | @Param('status', new ParseEnumPipe(CatStatus)) |
DefaultValuePipe | Supplies a fallback value when the client omits an optional query parameter. | @Query('page', new DefaultValuePipe(1)) |
ParseFilePipe | Validates uploaded files (size, mime-type) when using @UploadedFile(). | Multipart file uploads |
Using Built-in Pipes on Route Parameters
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:
- Parameter Level:
@Param('id', ParseIntPipe) id: number. - Method Level:
@UsePipes(new ValidationPipe())above a@Post()handler. - Controller Level:
@UsePipes(new ValidationPipe())above a@Controller('users')class. - Global Level: Applies across every route in the entire application. Registered in
main.tsusingapp.useGlobalPipes(new ValidationPipe())or through module dependency injection:
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.
Install Validation Dependencies
Install the official validation libraries needed for decorator-based validation and object transformation in NestJS:
npm install class-validator class-transformer
Create a Custom Pipe: PositiveIntPipe
We build a custom pipe ensuring route parameter numbers are strictly greater than zero:
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;
}
}
Define Validation Rules in the DTO
Decorate DTO fields with semantic rules provided by class-validator:
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;
}
Connect the DTO and Custom Pipe in the Controller
Apply validation in the controller. ValidationPipe validates the request body while PositiveIntPipe validates :id:
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);
}
}
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
}