NestJS Pipes

A pipe transforms or validates data before it reaches the controller method. Pipes sit between the raw request data and the handler parameters. They receive the incoming value, apply a transformation or validation, and either return the cleaned value or throw an exception that stops the request.

Two Jobs of a Pipe

Transformation:
  Input: "42" (string from URL)
  Pipe:  ParseIntPipe
  Output: 42 (number)

Validation:
  Input: { name: '', email: 'bad' }
  Pipe:  ValidationPipe
  Output: 400 Bad Request (throws)

Built-In Pipes

Pipe                  | Purpose
----------------------|------------------------------------------
ValidationPipe        | Validates request data against a DTO class
ParseIntPipe          | Converts string to integer; throws if invalid
ParseFloatPipe        | Converts string to float
ParseBoolPipe         | Converts 'true'/'false' string to boolean
ParseArrayPipe        | Parses a comma-separated string to an array
ParseUUIDPipe         | Validates and passes a UUID string
ParseEnumPipe         | Validates that value is in an enum
DefaultValuePipe      | Provides a default if the value is undefined

Using Built-In Pipes

// ParseIntPipe — route parameter
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
  return this.usersService.findOne(id);
}

// ParseBoolPipe — query string
@Get()
findAll(@Query('active', ParseBoolPipe) active: boolean) {
  return this.usersService.findAll({ active });
}

// DefaultValuePipe — optional query with fallback
@Get()
findAll(
  @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
  @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
) {
  return this.usersService.findAll({ page, limit });
}

Creating a Custom Pipe

A pipe is a class decorated with @Injectable() that implements the PipeTransform interface:

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

@Injectable()
export class PositiveIntPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    const parsed = parseInt(value, 10);

    if (isNaN(parsed)) {
      throw new BadRequestException(`'${value}' is not a valid number`);
    }

    if (parsed <= 0) {
      throw new BadRequestException(`Value must be a positive number`);
    }

    return parsed;
  }
}

// Usage
@Get(':id')
findOne(@Param('id', PositiveIntPipe) id: number) {
  return this.usersService.findOne(id);
}

Pipe Execution Position in the Pipeline

HTTP Request
     |
     v
Middleware
     |
     v
Guards
     |
     v
Interceptors (before)
     |
     v
Pipes  ← transform and validate parameters HERE
     |
     v
Controller Method
     |
     v
Interceptors (after)
     |
     v
HTTP Response

Applying Pipes at Different Scopes

// Parameter level — affects only this parameter
@Param('id', ParseIntPipe) id: number

// Method level — affects all parameters on this route
@Post()
@UsePipes(new ValidationPipe())
create(@Body() dto: CreateUserDto) { ... }

// Controller level — affects all methods in the controller
@Controller('users')
@UsePipes(ValidationPipe)
export class UsersController { ... }

// Global — affects every route in the application
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

Validation Pipe With ParseEnumPipe

enum UserRole {
  Admin = 'admin',
  User = 'user',
  Manager = 'manager',
}

@Get()
findByRole(@Query('role', new ParseEnumPipe(UserRole)) role: UserRole) {
  return this.usersService.findByRole(role);
}

// GET /users?role=admin      → works
// GET /users?role=superuser  → 400 Bad Request

Pipes vs ValidationPipe Internals

The built-in ValidationPipe is itself a pipe. It uses class-validator decorators on your DTO and class-transformer to convert the raw object into a typed class instance before running validation. Understanding that ValidationPipe is a pipe clarifies why it applies through the same @UsePipes() decorator or global registration as any other pipe.

ParseArrayPipe for Comma-Separated Inputs

// GET /users?ids=1,2,3,4
@Get()
findByIds(
  @Query('ids', new ParseArrayPipe({ items: Number, separator: ',' }))
  ids: number[],
) {
  return this.usersService.findByIds(ids);
}
// ids = [1, 2, 3, 4]

Pipes enforce the contract between the raw HTTP world and your typed TypeScript code. Every route parameter arrives as a string. Every query value arrives as a string. Pipes bridge that gap — converting strings to numbers, booleans, enums, and arrays, and rejecting values that do not conform to the expected type before any business logic runs.

Leave a Comment

Your email address will not be published. Required fields are marked *