Skip to content

NestJS Day 5: What are Pipes?

NestJS Day 5

Abstract

This is a concise, summarized approach to learn NestJS. For more in-depth knowledge about NestJS, visit the official NestJS documentation.

Github repo for day 5

Pipes in NestJS are powerful components that operate on the arguments being processed by controller route handlers. They have two primary use cases: transformation (converting input data to the desired form) and validation (evaluating input data and passing it through unchanged if valid, or throwing an exception if invalid).

Table of Contents

  1. What are Pipes?
  2. Built-in Pipes
  3. Binding Pipes
  4. Transformation Use Cases
  5. Validation Use Cases
  6. Custom Pipes
  7. Schema-based Validation
  8. Class Validator Integration
  9. Global Pipes
  10. Advanced Pipe Techniques
  11. Best Practices
  12. Real-world Examples

1. What are Pipes?

Pipes are classes annotated with the @Injectable() decorator that implement the PipeTransform interface. They run inside the exceptions zone, meaning when a pipe throws an exception, it’s handled by the exceptions layer and no controller method is executed afterward.

As you can see there are a bunch of decorators and their interface to remember, I created this small diagram to help you remember them:

Different Nestjs elements and their corresponding decorators and interfaces

Key Characteristics:

  • Transform data: Convert input data to desired format (string to integer)
  • Validate data: Ensure input data meets certain criteria
  • Run before method execution: Pipes intercept arguments before they reach route handlers
  • Exception handling: Failed validation/transformation throws exceptions automatically
  • Reusable: Can be applied at different scopes (parameter, method, controller, global)

Pipe Interface

Every pipe must implement the PipeTransform interface:

import { PipeTransform, ArgumentMetadata } from "@nestjs/common";
export interface PipeTransform<T = any, R = any> {
transform(value: T, metadata: ArgumentMetadata): R;
}

The transform() method has two parameters:

  • value: The currently processed method argument
  • metadata: The metadata of the currently processed method argument

2. Built-in Pipes

NestJS provides several built-in pipes available out-of-the-box:

Core Built-in Pipes

import {
ValidationPipe,
ParseIntPipe,
ParseFloatPipe,
ParseBoolPipe,
ParseArrayPipe,
ParseUUIDPipe,
ParseEnumPipe,
DefaultValuePipe,
ParseFilePipe,
ParseDatePipe,
} from "@nestjs/common";

ParseIntPipe

Converts string parameters to integers and validates they are numeric:

// Basic usage
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
// With custom error status
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}
// Query parameter example
@Get()
async findAll(@Query('age', ParseIntPipe) age: number) {
return this.catsService.findByAge(age);
}

ParseBoolPipe

Converts string values to boolean:

@Get()
async findAll(
@Query('activeOnly', ParseBoolPipe) activeOnly: boolean
) {
return this.catsService.findAll(activeOnly);
}
// Accepts: 'true', 'false', '1', '0'
// GET /cats?activeOnly=true
// GET /cats?activeOnly=1

ParseUUIDPipe

Validates and parses UUID strings:

@Get(':uuid')
async findOne(@Param('uuid', ParseUUIDPipe) uuid: string) {
return this.catsService.findOne(uuid);
}
// Specific UUID version
@Get(':uuid')
async findOne(
@Param('uuid', new ParseUUIDPipe({ version: '4' })) uuid: string
) {
return this.catsService.findOne(uuid);
}

ParseEnumPipe

Validates that a value is part of an enum:

enum Status {
ACTIVE = 'active',
INACTIVE = 'inactive',
PENDING = 'pending'
}
@Get()
async findByStatus(
@Query('status', new ParseEnumPipe(Status)) status: Status
) {
return this.catsService.findByStatus(status);
}

DefaultValuePipe

Provides default values for missing parameters:

@Get()
async findAll(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe)
activeOnly: boolean,
@Query('page', new DefaultValuePipe(0), ParseIntPipe)
page: number,
) {
return this.catsService.findAllPagination({ activeOnly, page });
}

ParseArrayPipe

Converts comma-separated values to arrays:

@Get()
async findByTags(
@Query('tags', new ParseArrayPipe({ items: String, separator: ',' }))
tags: string[]
) {
return this.catsService.findByTags(tags);
}
// GET /cats?tags=persian,siamese,maine-coon
// Result: ['persian', 'siamese', 'maine-coon']

NestJS Built-in Pipes Cheat Sheet

1. ValidationPipe

Purpose: Validates request data against class-validator decorators and transforms plain objects to class instances.

Use Cases:

  • Form validation
  • API request body validation
  • Query parameter validation
  • Auto-transformation of incoming data

Basic Usage:

// Global usage
app.useGlobalPipes(new ValidationPipe());
// Route-level usage
@Post()
@UsePipes(new ValidationPipe())
createUser(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
// Parameter-level usage
@Post()
createUser(@Body(new ValidationPipe()) createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}

Common Options:

new ValidationPipe({
whitelist: true, // Strip non-whitelisted properties
forbidNonWhitelisted: true, // Throw error for non-whitelisted properties
transform: true, // Auto-transform payloads to DTO instances
disableErrorMessages: false, // Disable detailed error messages
validateCustomDecorators: true, // Enable custom validators
});

2. ParseIntPipe

Purpose: Transforms string parameters to integers and validates they are valid numbers.

Use Cases:

  • URL path parameters (e.g., /users/:id)
  • Query parameters that should be numbers
  • Route parameters requiring integer validation

Usage:

// Path parameter
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.userService.findOne(id);
}
// Query parameter
@Get()
findAll(@Query('page', ParseIntPipe) page: number) {
return this.userService.findAll(page);
}
// With custom error message
@Get(':id')
findOne(@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE })) id: number) {
return this.userService.findOne(id);
}

3. ParseFloatPipe

Purpose: Transforms string parameters to floating-point numbers.

Use Cases:

  • Price calculations
  • Coordinates (latitude, longitude)
  • Percentage values
  • Any decimal number inputs

Usage:

// Query parameter for price filtering
@Get('products')
getProducts(@Query('minPrice', ParseFloatPipe) minPrice: number) {
return this.productService.findByMinPrice(minPrice);
}
// Path parameter for coordinates
@Get('location/:lat/:lng')
getLocation(
@Param('lat', ParseFloatPipe) latitude: number,
@Param('lng', ParseFloatPipe) longitude: number
) {
return this.locationService.findByCoordinates(latitude, longitude);
}

4. ParseBoolPipe

Purpose: Transforms string values to boolean values.

Use Cases:

  • Feature toggles
  • Filter flags (active/inactive)
  • Boolean query parameters
  • Configuration options

Usage:

// Query parameter for filtering
@Get('users')
getUsers(@Query('isActive', ParseBoolPipe) isActive: boolean) {
return this.userService.findByStatus(isActive);
}
// Feature toggle
@Get('products')
getProducts(@Query('includeDiscounted', ParseBoolPipe) includeDiscounted: boolean) {
return this.productService.findAll(includeDiscounted);
}

Accepted Values: true, false, 'true', 'false', '1', '0', 1, 0

5. ParseArrayPipe

Purpose: Transforms comma-separated strings or arrays into typed arrays.

Use Cases:

  • Multiple IDs selection
  • Tag filtering
  • Multiple category selection
  • Bulk operations

Usage:

// Query parameter for multiple IDs
@Get()
findByIds(@Query('ids', new ParseArrayPipe({ items: Number })) ids: number[]) {
return this.userService.findByIds(ids);
}
// String array
@Get('products')
findProducts(@Query('tags', new ParseArrayPipe({ items: String, separator: ',' })) tags: string[]) {
return this.productService.findByTags(tags);
}
// With custom separator
@Get()
findUsers(@Query('roles', new ParseArrayPipe({ items: String, separator: '|' })) roles: string[]) {
return this.userService.findByRoles(roles);
}

6. ParseUUIDPipe

Purpose: Validates and ensures parameters are valid UUIDs.

Use Cases:

  • Database primary keys using UUIDs
  • Session IDs
  • API keys validation
  • Resource identifiers

Usage:

// Path parameter validation
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.userService.findOne(id);
}
// Specific UUID version
@Get(':id')
findOne(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
return this.userService.findOne(id);
}
// Query parameter
@Get()
findBySession(@Query('sessionId', ParseUUIDPipe) sessionId: string) {
return this.sessionService.findBySessionId(sessionId);
}

Supported Versions: '3', '4', '5', 'all' (default)

7. ParseEnumPipe

Purpose: Validates that a parameter value exists in a specified enum.

Use Cases:

  • Status filtering (ACTIVE, INACTIVE, PENDING)
  • User roles validation
  • Order status
  • Category types

Usage:

// Define enum
enum UserRole {
ADMIN = 'admin',
USER = 'user',
MODERATOR = 'moderator'
}
enum Status {
ACTIVE = 'active',
INACTIVE = 'inactive',
PENDING = 'pending'
}
// Usage in controller
@Get()
findByRole(@Query('role', new ParseEnumPipe(UserRole)) role: UserRole) {
return this.userService.findByRole(role);
}
@Get('orders')
getOrders(@Query('status', new ParseEnumPipe(Status)) status: Status) {
return this.orderService.findByStatus(status);
}

8. DefaultValuePipe

Purpose: Provides default values for parameters when they are undefined or null.

Use Cases:

  • Pagination defaults
  • Default sorting options
  • Optional query parameters
  • Configuration defaults

Usage:

// Pagination defaults
@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number
) {
return this.userService.findAll(page, limit);
}
// Default sorting
@Get('products')
getProducts(
@Query('sortBy', new DefaultValuePipe('createdAt')) sortBy: string,
@Query('order', new DefaultValuePipe('DESC')) order: string
) {
return this.productService.findAll(sortBy, order);
}
// Boolean default
@Get()
getUsers(@Query('includeInactive', new DefaultValuePipe(false), ParseBoolPipe) includeInactive: boolean) {
return this.userService.findAll(includeInactive);
}

9. ParseFilePipe

Purpose: Validates uploaded files including size, type, and other constraints.

Use Cases:

  • File upload validation
  • Image upload restrictions
  • Document type validation
  • File size limitations

Usage:

// Basic file validation
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile(ParseFilePipe) file: Express.Multer.File) {
return this.fileService.upload(file);
}
// With validators
@Post('upload-image')
@UseInterceptors(FileInterceptor('image'))
uploadImage(
@UploadedFile(
new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 1024 * 1024 * 5 }), // 5MB
new FileTypeValidator({ fileType: /^image\/(jpeg|png|gif)$/ })
]
})
)
file: Express.Multer.File
) {
return this.imageService.upload(file);
}
// Optional file upload
@Post('upload-optional')
@UseInterceptors(FileInterceptor('file'))
uploadOptional(
@UploadedFile(
new ParseFilePipe({
fileIsRequired: false,
validators: [
new MaxFileSizeValidator({ maxSize: 1024 * 1024 * 2 })
]
})
)
file?: Express.Multer.File
) {
return file ? this.fileService.upload(file) : { message: 'No file uploaded' };
}

Built-in Validators:

  • MaxFileSizeValidator: File size limits
  • FileTypeValidator: MIME type validation

10. ParseDatePipe

Purpose: Transforms string parameters into Date objects with validation.

Use Cases:

  • Date range filtering
  • Scheduling parameters
  • Event date validation
  • Timestamp parsing

Usage:

// Query parameter for date filtering
@Get('events')
getEvents(
@Query('startDate', ParseDatePipe) startDate: Date,
@Query('endDate', ParseDatePipe) endDate: Date
) {
return this.eventService.findByDateRange(startDate, endDate);
}
// Path parameter
@Get('schedule/:date')
getSchedule(@Param('date', ParseDatePipe) date: Date) {
return this.scheduleService.findByDate(date);
}
// Optional date with default
@Get('reports')
getReports(
@Query('from', new DefaultValuePipe(new Date()), ParseDatePipe) from: Date
) {
return this.reportService.findFrom(from);
}

Accepted Formats:

  • ISO 8601 strings: 2023-12-25T10:30:00Z
  • Date strings: 2023-12-25
  • Timestamp numbers: 1703505000000

3. Binding Pipes

Pipes can be bound at different scopes to control where they apply:

Parameter-scoped Pipes

Applied to individual parameters:

// Class binding (recommended)
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
// Instance binding (for custom configuration)
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}

Method-scoped Pipes

Applied to all parameters of a method:

@Post()
@UsePipes(ValidationPipe)
async create(@Body() createCatDto: CreateCatDto) {
return this.catsService.create(createCatDto);
}
// Multiple pipes
@Post()
@UsePipes(ValidationPipe, TransformPipe)
async create(@Body() createCatDto: CreateCatDto) {
return this.catsService.create(createCatDto);
}

Controller-scoped Pipes

Applied to all methods in a controller:

@Controller("cats")
@UsePipes(ValidationPipe)
export class CatsController {
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return this.catsService.create(createCatDto);
}
@Put(":id")
async update(@Param("id") id: string, @Body() updateCatDto: UpdateCatDto) {
return this.catsService.update(id, updateCatDto);
}
}

Global Pipes

Applied to every route handler across the entire application:

// main.ts - Instance binding
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
// app.module.ts - Dependency injection (recommended)
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}

4. Transformation Use Cases

Transformation pipes convert input data to the desired format:

Basic String to Number Transformation

// Custom ParseIntPipe implementation
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed (numeric string is expected)');
}
return val;
}
}
// Usage
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
console.log(typeof id); // number
return this.catsService.findOne(id);
}

User Entity Transformation

Transform an ID to a full user entity:

@Injectable()
export class UserByIdPipe implements PipeTransform<string, Promise<UserEntity>> {
constructor(private usersService: UsersService) {}
async transform(value: string, metadata: ArgumentMetadata): Promise<UserEntity> {
const user = await this.usersService.findById(value);
if (!user) {
throw new NotFoundException(`User with ID ${value} not found`);
}
return user;
}
}
// Usage - parameter becomes the full user entity
@Get(':id')
async getUser(@Param('id', UserByIdPipe) user: UserEntity) {
return user; // Full user object, not just the ID
}

Trim and Normalize String

@Injectable()
export class TrimPipe implements PipeTransform<string, string> {
transform(value: string, metadata: ArgumentMetadata): string {
return typeof value === 'string' ? value.trim().toLowerCase() : value;
}
}
@Get()
async search(@Query('q', TrimPipe) query: string) {
return this.searchService.search(query);
}

5. Validation Use Cases

Validation pipes ensure data meets specific criteria before processing:

Simple Validation Pipe

@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
// Simple validation logic
if (!value) {
throw new BadRequestException("Value is required");
}
return value;
}
}

Age Validation Pipe

@Injectable()
export class AgeValidationPipe implements PipeTransform<number, number> {
transform(value: number, metadata: ArgumentMetadata): number {
if (value < 0 || value > 150) {
throw new BadRequestException('Age must be between 0 and 150');
}
return value;
}
}
@Post()
async create(
@Body('age', ParseIntPipe, AgeValidationPipe) age: number,
@Body() createCatDto: CreateCatDto
) {
return this.catsService.create({ ...createCatDto, age });
}

6. Custom Pipes

Creating custom pipes for specific business logic:

File Size Validation Pipe

@Injectable()
export class FileSizeValidationPipe implements PipeTransform {
constructor(private readonly maxSizeInBytes: number) {}
transform(value: Express.Multer.File, metadata: ArgumentMetadata) {
if (value.size > this.maxSizeInBytes) {
throw new BadRequestException(
`File size exceeds limit of ${this.maxSizeInBytes} bytes`
);
}
return value;
}
}
// Usage
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
async uploadFile(
@UploadedFile(new FileSizeValidationPipe(1024 * 1024)) // 1MB limit
file: Express.Multer.File
) {
return this.fileService.save(file);
}

Password Strength Validation Pipe

@Injectable()
export class PasswordValidationPipe implements PipeTransform<string, string> {
transform(value: string, metadata: ArgumentMetadata): string {
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
if (!passwordRegex.test(value)) {
throw new BadRequestException(
'Password must contain at least 8 characters, including uppercase, lowercase, number, and special character'
);
}
return value;
}
}
@Post('register')
async register(
@Body('password', PasswordValidationPipe) password: string,
@Body() userData: CreateUserDto
) {
return this.authService.register({ ...userData, password });
}

7. Schema-based Validation

Using external libraries for comprehensive validation:

Zod Schema Validation

Install Zod first:

Terminal window
npm install --save zod

Create a Zod validation pipe:

import {
PipeTransform,
ArgumentMetadata,
BadRequestException,
} from "@nestjs/common";
import { ZodSchema } from "zod";
export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodSchema) {}
transform(value: unknown, metadata: ArgumentMetadata) {
try {
const parsedValue = this.schema.parse(value);
return parsedValue;
} catch (error) {
throw new BadRequestException("Validation failed");
}
}
}

Define Zod schema:

create-cat.dto.ts
import { z } from "zod";
export const createCatSchema = z
.object({
name: z.string().min(1).max(50),
age: z.number().int().min(0).max(30),
breed: z.string().min(1),
isActive: z.boolean().default(true),
})
.required();
export type CreateCatDto = z.infer<typeof createCatSchema>;

Use with controller:

@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
return this.catsService.create(createCatDto);
}

Joi Schema Validation

import * as Joi from "joi";
export const createCatSchema = Joi.object({
name: Joi.string().required().min(1).max(50),
age: Joi.number().integer().min(0).max(30).required(),
breed: Joi.string().required(),
isActive: Joi.boolean().default(true),
});
@Injectable()
export class JoiValidationPipe implements PipeTransform {
constructor(private schema: Joi.ObjectSchema) {}
transform(value: any, metadata: ArgumentMetadata) {
const { error, value: validatedValue } = this.schema.validate(value);
if (error) {
throw new BadRequestException("Validation failed");
}
return validatedValue;
}
}

8. Class Validator Integration

Using class-validator and class-transformer for decorator-based validation:

Installation

Terminal window
npm install --save class-validator class-transformer

DTO with Validation Decorators

create-cat.dto.ts
import {
IsString,
IsInt,
IsBoolean,
IsOptional,
Min,
Max,
Length,
IsEmail,
IsEnum,
ValidateNested,
Type,
} from "class-validator";
export enum CatBreed {
PERSIAN = "persian",
SIAMESE = "siamese",
MAINE_COON = "maine_coon",
}
export class OwnerDto {
@IsString()
@Length(2, 50)
name: string;
@IsEmail()
email: string;
}
export class CreateCatDto {
@IsString()
@Length(1, 50)
name: string;
@IsInt()
@Min(0)
@Max(30)
age: number;
@IsEnum(CatBreed)
breed: CatBreed;
@IsBoolean()
@IsOptional()
isActive?: boolean = true;
@ValidateNested()
@Type(() => OwnerDto)
owner: OwnerDto;
}

Custom Validation Pipe with Class Validator

import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from "@nestjs/common";
import { validate } from "class-validator";
import { plainToInstance } from "class-transformer";
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToInstance(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
const errorMessage = errors
.map((error) => Object.values(error.constraints || {}))
.flat()
.join(", ");
throw new BadRequestException(`Validation failed: ${errorMessage}`);
}
return object;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}

Usage with Detailed Error Handling

@Injectable()
export class DetailedValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToInstance(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
const formattedErrors = errors.map((error) => ({
field: error.property,
errors: Object.values(error.constraints || {}),
value: error.value,
}));
throw new BadRequestException({
message: "Validation failed",
errors: formattedErrors,
});
}
return object;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}

Parameter-scoped Class Validation

@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto
) {
return this.catsService.create(createCatDto);
}

9. Global Pipes

Setting up application-wide pipes:

Method 1: Using useGlobalPipes()

main.ts
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Strip properties not in DTO
forbidNonWhitelisted: true, // Throw error for extra properties
transform: true, // Transform payloads to DTO instances
disableErrorMessages: false, // Show detailed error messages
transformOptions: {
enableImplicitConversion: true, // Auto-convert types
},
})
);
await app.listen(3000);
}
bootstrap();
app.module.ts
import { Module } from "@nestjs/common";
import { APP_PIPE } from "@nestjs/core";
import { ValidationPipe } from "@nestjs/common";
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}

Multiple Global Pipes

app.module.ts
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
{
provide: APP_PIPE,
useClass: TransformPipe,
},
],
})
export class AppModule {}

Conditional Global Pipes

conditional-pipe.provider.ts
import { Provider } from "@nestjs/common";
import { APP_PIPE } from "@nestjs/core";
export const conditionalPipeProvider: Provider = {
provide: APP_PIPE,
useFactory: () => {
if (process.env.NODE_ENV === "development") {
return new ValidationPipe({
disableErrorMessages: false,
skipMissingProperties: false,
});
}
return new ValidationPipe({
disableErrorMessages: true,
skipMissingProperties: true,
});
},
};

10. Advanced Pipe Techniques

Async Pipes

Pipes can be asynchronous for database lookups or external API calls:

@Injectable()
export class AsyncValidationPipe implements PipeTransform {
constructor(private userService: UserService) {}
async transform(value: any, metadata: ArgumentMetadata) {
// Async validation logic
const exists = await this.userService.emailExists(value.email);
if (exists) {
throw new ConflictException("Email already exists");
}
return value;
}
}

Conditional Validation

@Injectable()
export class ConditionalValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
const { type, data } = metadata;
// Only validate body parameters
if (type === "body") {
return this.validateBody(value);
}
// Only validate specific parameter names
if (data === "email") {
return this.validateEmail(value);
}
return value;
}
private validateBody(value: any) {
// Body validation logic
return value;
}
private validateEmail(value: string) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new BadRequestException("Invalid email format");
}
return value;
}
}

Metadata-aware Pipes

@Injectable()
export class MetadataAwarePipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
console.log("Metadata:", metadata);
/*
{
type: 'body' | 'query' | 'param' | 'custom',
metatype: [Function: CreateCatDto],
data: undefined
}
*/
if (metadata.metatype === CreateCatDto) {
// Specific validation for CreateCatDto
return this.validateCreateCatDto(value);
}
return value;
}
private validateCreateCatDto(value: any) {
// Custom validation logic for CreateCatDto
return value;
}
}

Pipe Chaining

@Get(':id')
async findOne(
@Param('id', ParseIntPipe, PositiveNumberPipe, ExistsValidationPipe)
id: number
) {
return this.catsService.findOne(id);
}
// Execution order: ParseIntPipe -> PositiveNumberPipe -> ExistsValidationPipe

11. Best Practices

1. Use Built-in Pipes When Possible

// Preferred: Use built-in ParseIntPipe
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
// Avoid: Custom implementation when built-in exists
@Get(':id')
async findOne(@Param('id', CustomParseIntPipe) id: number) {
return this.catsService.findOne(id);
}

2. Prefer Class-based Registration

// Preferred: Class-based registration
@UsePipes(ValidationPipe)
// Avoid: Instance-based registration (unless configuration needed)
@UsePipes(new ValidationPipe())

3. Global vs Scoped Pipes

// Global pipes for common validation
app.useGlobalPipes(new ValidationPipe());
// Scoped pipes for specific validation
@Post()
@UsePipes(new SpecificValidationPipe())
async create(@Body() data: CreateDto) { }

4. Error Handling Best Practices

@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
try {
// Validation logic
return this.validate(value);
} catch (error) {
// Provide clear, helpful error messages
throw new BadRequestException({
message: "Validation failed",
field: metadata.data,
expectedType: metadata.type,
receivedValue: value,
});
}
}
}

5. Type Safety

// Use proper TypeScript types
@Injectable()
export class TypedPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const result = parseInt(value, 10);
if (isNaN(result)) {
throw new BadRequestException("Expected numeric string");
}
return result;
}
}

6. Reusable Validation Logic

// Create reusable validation functions
export class ValidationUtils {
static validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
static validatePhoneNumber(phone: string): boolean {
const phoneRegex = /^\+?[\d\s-()]+$/;
return phoneRegex.test(phone);
}
}
@Injectable()
export class ContactValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
if (value.email && !ValidationUtils.validateEmail(value.email)) {
throw new BadRequestException("Invalid email format");
}
if (value.phone && !ValidationUtils.validatePhoneNumber(value.phone)) {
throw new BadRequestException("Invalid phone number format");
}
return value;
}
}

12. Real-world Examples

Example 1: E-commerce Product Validation

product.dto.ts
import {
IsString,
IsNumber,
IsOptional,
Min,
Max,
IsArray,
ValidateNested,
Type,
} from "class-validator";
export class ProductImageDto {
@IsString()
url: string;
@IsString()
altText: string;
}
export class CreateProductDto {
@IsString()
@Length(1, 100)
name: string;
@IsString()
@Length(10, 1000)
description: string;
@IsNumber({ maxDecimalPlaces: 2 })
@Min(0.01)
@Max(999999.99)
price: number;
@IsNumber()
@Min(0)
stock: number;
@IsArray()
@ValidateNested({ each: true })
@Type(() => ProductImageDto)
images: ProductImageDto[];
@IsArray()
@IsString({ each: true })
@IsOptional()
tags?: string[];
}
// Custom pipes for product validation
@Injectable()
export class ProductSkuGeneratorPipe implements PipeTransform {
transform(value: CreateProductDto, metadata: ArgumentMetadata) {
// Generate SKU based on product name
const sku = value.name
.toLowerCase()
.replace(/[^a-z0-9]/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
return {
...value,
sku: `${sku}-${Date.now()}`,
};
}
}
@Injectable()
export class PriceValidationPipe implements PipeTransform {
async transform(value: CreateProductDto, metadata: ArgumentMetadata) {
// Check if price is competitive
const category = await this.categoryService.findByName(value.category);
const avgPrice = await this.productService.getAveragePrice(category.id);
if (value.price > avgPrice * 2) {
throw new BadRequestException(
`Price ${value.price} is significantly higher than category average ${avgPrice}`
);
}
return value;
}
}
// Usage in controller
@Controller("products")
export class ProductsController {
@Post()
@UsePipes(ValidationPipe, ProductSkuGeneratorPipe, PriceValidationPipe)
async create(@Body() createProductDto: CreateProductDto) {
return this.productService.create(createProductDto);
}
}

Example 2: File Upload Validation Pipeline

@Injectable()
export class FileTypeValidationPipe implements PipeTransform {
private readonly allowedMimeTypes = [
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
];
transform(value: Express.Multer.File, metadata: ArgumentMetadata) {
if (!this.allowedMimeTypes.includes(value.mimetype)) {
throw new BadRequestException(
`File type ${value.mimetype} not allowed. Allowed types: ${this.allowedMimeTypes.join(', ')}`
);
}
return value;
}
}
@Injectable()
export class FileSizeValidationPipe implements PipeTransform {
constructor(private readonly maxSizeInMB: number) {}
transform(value: Express.Multer.File, metadata: ArgumentMetadata) {
const maxSizeInBytes = this.maxSizeInMB * 1024 * 1024;
if (value.size > maxSizeInBytes) {
throw new BadRequestException(
`File size ${(value.size / 1024 / 1024).toFixed(2)}MB exceeds limit of ${this.maxSizeInMB}MB`
);
}
return value;
}
}
@Injectable()
export class ImageDimensionValidationPipe implements PipeTransform {
async transform(value: Express.Multer.File, metadata: ArgumentMetadata) {
if (!value.mimetype.startsWith('image/')) {
return value; // Skip non-images
}
const dimensions = await this.getImageDimensions(value.buffer);
if (dimensions.width > 4000 || dimensions.height > 4000) {
throw new BadRequestException(
`Image dimensions ${dimensions.width}x${dimensions.height} exceed maximum of 4000x4000`
);
}
if (dimensions.width < 100 || dimensions.height < 100) {
throw new BadRequestException(
`Image dimensions ${dimensions.width}x${dimensions.height} below minimum of 100x100`
);
}
return {
...value,
metadata: {
width: dimensions.width,
height: dimensions.height,
},
};
}
private async getImageDimensions(buffer: Buffer): Promise<{ width: number; height: number }> {
// Implementation to get image dimensions
// Could use sharp, jimp, or similar library
return { width: 1920, height: 1080 }; // Placeholder
}
}
// Usage
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
async uploadFile(
@UploadedFile(
FileTypeValidationPipe,
new FileSizeValidationPipe(10), // 10MB limit
ImageDimensionValidationPipe
)
file: Express.Multer.File & { metadata?: any }
) {
return this.fileService.save(file);
}

Example 3: API Pagination and Filtering

pagination.dto.ts
export class PaginationDto {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit?: number = 10;
@IsOptional()
@IsString()
search?: string;
@IsOptional()
@IsString()
sortBy?: string;
@IsOptional()
@IsEnum(['ASC', 'DESC'])
sortOrder?: 'ASC' | 'DESC' = 'ASC';
}
@Injectable()
export class PaginationValidationPipe implements PipeTransform {
transform(value: PaginationDto, metadata: ArgumentMetadata) {
// Calculate offset
const offset = (value.page - 1) * value.limit;
// Validate sort field if provided
const allowedSortFields = ['id', 'name', 'createdAt', 'updatedAt'];
if (value.sortBy && !allowedSortFields.includes(value.sortBy)) {
throw new BadRequestException(
`Invalid sort field. Allowed fields: ${allowedSortFields.join(', ')}`
);
}
return {
...value,
offset,
take: value.limit,
};
}
}
@Injectable()
export class SearchSanitizationPipe implements PipeTransform {
transform(value: PaginationDto, metadata: ArgumentMetadata) {
if (value.search) {
// Sanitize search query
value.search = value.search
.trim()
.replace(/[<>]/g, '') // Remove potential XSS characters
.substring(0, 100); // Limit search length
}
return value;
}
}
// Usage
@Get()
async findAll(
@Query(ValidationPipe, PaginationValidationPipe, SearchSanitizationPipe)
query: PaginationDto
) {
return this.catsService.findAll(query);
}

Testing Pipes

Unit Testing Custom Pipes

parse-int.pipe.spec.ts
import { BadRequestException } from "@nestjs/common";
import { ParseIntPipe } from "./parse-int.pipe";
describe("ParseIntPipe", () => {
let pipe: ParseIntPipe;
beforeEach(() => {
pipe = new ParseIntPipe();
});
it("should be defined", () => {
expect(pipe).toBeDefined();
});
it("should parse valid integer string", () => {
const result = pipe.transform("123", {
type: "param",
metatype: Number,
data: "id",
});
expect(result).toBe(123);
});
it("should throw BadRequestException for invalid string", () => {
expect(() => {
pipe.transform("abc", { type: "param", metatype: Number, data: "id" });
}).toThrow(BadRequestException);
});
it("should handle edge cases", () => {
expect(
pipe.transform("0", { type: "param", metatype: Number, data: "id" })
).toBe(0);
expect(() => {
pipe.transform("", { type: "param", metatype: Number, data: "id" });
}).toThrow(BadRequestException);
});
});

Integration Testing with Controllers

cats.controller.spec.ts
import { Test, TestingModule } from "@nestjs/testing";
import { BadRequestException } from "@nestjs/common";
import { CatsController } from "./cats.controller";
import { CatsService } from "./cats.service";
import { ValidationPipe } from "./pipes/validation.pipe";
describe("CatsController", () => {
let controller: CatsController;
let service: CatsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CatsController],
providers: [
{
provide: CatsService,
useValue: {
create: jest.fn(),
findAll: jest.fn(),
findOne: jest.fn(),
},
},
],
}).compile();
controller = module.get<CatsController>(CatsController);
service = module.get<CatsService>(CatsService);
});
describe("create", () => {
it("should create a cat with valid data", async () => {
const createCatDto = {
name: "Fluffy",
age: 3,
breed: "persian",
};
const pipe = new ValidationPipe();
const validatedData = await pipe.transform(createCatDto, {
type: "body",
metatype: CreateCatDto,
});
expect(validatedData).toEqual(createCatDto);
});
it("should throw error with invalid data", async () => {
const invalidDto = {
name: "",
age: -1,
breed: "persian",
};
const pipe = new ValidationPipe();
await expect(
pipe.transform(invalidDto, {
type: "body",
metatype: CreateCatDto,
})
).rejects.toThrow(BadRequestException);
});
});
});

Summary

Pipes in NestJS are essential components for data transformation and validation:

  1. Built-in pipes handle common transformation and validation scenarios
  2. Custom pipes implement specific business logic and validation rules
  3. Multiple scopes (parameter, method, controller, global) provide flexibility
  4. Schema validation with Zod, Joi, or class-validator offers robust validation
  5. Async pipes enable database lookups and external API validation
  6. Proper error handling provides clear feedback to API consumers

Remember to:

  • Use built-in pipes when possible for common scenarios
  • Implement custom pipes for specific business logic
  • Prefer class-based registration for dependency injection
  • Provide clear, helpful error messages
  • Test pipes thoroughly in isolation and integration
  • Consider performance implications for global pipes
  • Use TypeScript types for better type safety

Pipes are crucial for building robust, secure NestJS applications that properly validate and transform incoming data before it reaches your business logic.

Key Takeaways

  • Transformation: Convert input data to desired formats (string to number, ID to entity)
  • Validation: Ensure data meets business rules and constraints
  • Built-in Pipes: Use ParseIntPipe, ValidationPipe, ParseUUIDPipe, etc. for common cases
  • Custom Pipes: Implement PipeTransform interface for specific logic
  • Global Registration: Use APP_PIPE token for dependency injection
  • Schema Validation: Leverage Zod, Joi, or class-validator for comprehensive validation
  • Error Handling: Pipes throw exceptions that are caught by exception filters
  • Type Safety: Use proper TypeScript types for better development experience