NestJS — Enterprise Node.js

Pipes: Transform & Validate

16 min Lesson 11 of 48

Pipes: Transform & Validate

A pipe sits between the incoming request and your handler, doing one of two jobs: transformation (convert input to the desired form) or validation (check input and throw if it is invalid). The ValidationPipe from the previous lesson is just one built-in pipe — now let us understand the mechanism.

Built-in pipes

NestJS ships several ready-to-use pipes. The most common convert string route params into typed values:

import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common'; @Controller('users') export class UsersController { @Get(':id') findOne(@Param('id', ParseIntPipe) id: number) { // id is a real number; a non-numeric value throws 400 automatically return this.usersService.findOne(id); } }

Other built-ins include ParseUUIDPipe, ParseBoolPipe, ParseArrayPipe, ParseEnumPipe, and DefaultValuePipe.

Remember: route params and query strings always arrive as strings. ParseIntPipe turns '42' into the number 42 — and rejects 'abc' with a 400 before your handler runs.

Where pipes can be applied

  • Parameter-level — on a single @Param()/@Body() (most precise).
  • Method-level — with @UsePipes() on a handler.
  • Globalapp.useGlobalPipes() in main.ts (applies everywhere).

Writing a custom pipe

A custom pipe implements PipeTransform and a single transform(value, metadata) method. Whatever it returns becomes the handler's argument; whatever it throws becomes the error response:

import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common'; @Injectable() export class TrimPipe implements PipeTransform { transform(value: any) { if (typeof value !== 'string') { throw new BadRequestException('Expected a string'); } return value.trim(); } } // usage @Post() create(@Body('name', TrimPipe) name: string) {}

The metadata argument

The second parameter tells the pipe what it is processing — the type (body, query, param), the expected metatype, and the parameter name. This is how a generic pipe like ValidationPipe knows which DTO class to validate against:

transform(value: any, metadata: ArgumentMetadata) { // metadata.type -> 'body' | 'query' | 'param' | 'custom' // metadata.metatype -> the expected class (e.g. CreateUserDto) // metadata.data -> the @Body('name') key, if any return value; }
Pipes run before the handler, in order. A parameter can have several pipes; they execute left to right, each receiving the previous one's output — so you can transform then validate in a chain.
Keep pipes focused. A pipe should transform or validate a single input — not perform business logic, database lookups, or side effects. Those belong in services.

Summary

Pipes transform or validate inputs before they reach your handler. Use built-in pipes like ParseIntPipe to convert and guard route params, and write custom pipes by implementing PipeTransform.transform(). The metadata argument lets generic pipes inspect what they are processing. Pipes can be bound at parameter, method, or global level. Next: middleware, which runs even earlier in the lifecycle.