Suggest an editImprove this articleRefine the answer for “What are pipes in NestJS?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Pipes in NestJS** are functions that transform and validate incoming request data before it reaches the route handler, implementing the `PipeTransform` interface. ```typescript @Get(':id') findOne(@Param('id', ParseIntPipe) id: number) { return this.usersService.findOne(id); // id is number or 400 } ``` **Key point:** Express always sends URL params as strings; pipes convert them to the correct type or throw a 400 before the handler runs.Shown above the full answer for quick recall.Answer (EN)Image**Pipes in NestJS** are functions that intercept incoming request data (params, query, body) before it reaches the route handler, implementing the `PipeTransform` interface. They either transform the raw value into the correct type or throw an HTTP 400 if the input is invalid. ## Theory ### TL;DR - Think of a pipe as an airport security scanner: raw strings from the URL get checked and converted; bad input is rejected at the door, valid input passes through as the right type - Two jobs: transform (`"123"` → `123`) and validate (reject non-integers, invalid emails, malformed UUIDs) - HTTP always delivers params and query values as strings. Pipes fix that automatically - `ValidationPipe` for DTO validation; `ParseIntPipe`, `ParseUUIDPipe` for primitives - Decision rule: always pipe incoming HTTP data; skip it only for trusted internal calls ### Quick example ```typescript // Without pipe: id is always a string from the URL @Get(':id') findOne(@Param('id') id: string) { return this.usersService.findOne(+id); // manual parse, crashes on "abc" } // With ParseIntPipe: auto-parses and validates, throws 400 on failure @Get(':id') findOne(@Param('id', ParseIntPipe) id: number) { return this.usersService.findOne(id); // id is number or 400 Bad Request } // GET /users/123 → id = 123 // GET /users/abc → 400 "Validation failed (numeric string is expected)" ``` `ParseIntPipe` runs before the handler body. If the value is not a valid integer, NestJS throws `BadRequestException` and the handler never executes. ### Key difference from manual parsing Manual parsing inside the handler (`+id`, `parseInt(id)`) mixes data validation with business logic. On bad input it silently produces `NaN`, which then propagates into your service and causes failures far from the source. Pipes catch bad data at the boundary, before any logic runs, and return a consistent 400 response. ### When to use - **URL params and query strings**: always apply `ParseIntPipe`, `ParseUUIDPipe`, or similar. Express delivers them as strings. - **POST/PUT request body**: use `ValidationPipe` with a DTO class and class-validator decorators. - **Optional query params with defaults**: chain `DefaultValuePipe` before `ParseIntPipe`. - **Global validation**: `app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }))` in `main.ts`. - **Business-level checks** (email uniqueness, DB existence): custom pipe implementing `PipeTransform`. ### Built-in pipes | Pipe | What it does | Example | |---|---|---| | `ValidationPipe` | Validates DTOs via class-validator | `{ age: "25" }` → `{ age: 25 }` or 400 | | `ParseIntPipe` | string → integer, rejects non-integers | `"123"` → 123; `"abc"` → 400 | | `ParseFloatPipe` | string → float | `"12.5"` → 12.5 | | `ParseBoolPipe` | string → boolean | `"true"` → true | | `ParseUUIDPipe` | Validates UUID format | invalid UUID → 400 | | `ParseEnumPipe` | Validates against an enum | value not in enum → 400 | | `DefaultValuePipe` | Returns default when param is missing | no `limit` → 10 | | `ParseArrayPipe` | Comma-separated string → array | `"1,2,3"` → [1, 2, 3] | ### Binding levels Pipes can be applied at four levels. More specific always wins over less specific. ```typescript // 1. Parameter level (most specific) @Get(':id') findOne(@Param('id', ParseIntPipe) id: number) {} // 2. Method level @Post() @UsePipes(ValidationPipe) create(@Body() dto: CreateUserDto) {} // 3. Controller level @Controller('users') @UsePipes(new ValidationPipe()) export class UsersController {} // 4. Global (in main.ts) app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true })); // or via DI token: { provide: APP_PIPE, useClass: ValidationPipe } ``` Global pipes run on every request across the entire app. That is the right default for `ValidationPipe`. For type-specific parsing like `ParseIntPipe`, apply it at the parameter level so it only runs where a number is actually expected. ### How NestJS executes pipes During route registration, NestJS binds pipes to specific arguments using metadata from decorators (`@Param`, `@Body`, `@Query`). On each request, it calls `transform(value, metadata)` on every pipe in sequence before passing arguments to the handler. If any `transform()` throws a `BadRequestException`, the exception filter returns 400 and the handler is never called. Custom pipes can be `async`. NestJS awaits the result before continuing, so DB lookups inside `transform()` work without any extra setup. ### Common mistakes **Mistake 1:** `new ValidationPipe()` without `{ transform: true }`. Without it, DTO fields stay as strings even when typed as `number`. Validators like `@IsNumber()` then fail on the string value. From what I've seen in production codebases, this is the single most common pipe mistake. Everything looks correct until a numeric comparison silently breaks. ```typescript // Wrong: age stays "25" (string), @IsNumber() fails app.useGlobalPipes(new ValidationPipe()); // Correct app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true })); ``` **Mistake 2:** `@Body(ValidationPipe)` without a DTO class. `ValidationPipe` expects a class with decorator metadata. Without a DTO it has nothing to validate against and throws a cryptic error. ```typescript // Wrong @Post() create(@Body(ValidationPipe) body: any) {} // Correct: always pair with a DTO @Post() create(@Body() dto: CreateUserDto) {} ``` **Mistake 3:** Wrong pipe order on the same parameter. Pipes execute left to right. Putting `ValidationPipe` before `ParseIntPipe` means it tries to validate a string, not a number. ```typescript // Wrong: validates string first, then parses @Param('id', ValidationPipe, ParseIntPipe) // Correct: parse first, then validate @Param('id', ParseIntPipe, ValidationPipe) ``` **Mistake 4:** Throwing `new Error()` in a custom pipe. A plain `Error` becomes a 500 Internal Server Error. Pipes sit in the HTTP layer and should throw NestJS HTTP exceptions. ```typescript // Wrong: 500 response throw new Error('Invalid value'); // Correct: 400 response throw new BadRequestException('Invalid value'); ``` **Mistake 5:** Missing `async/await` in a custom pipe that does a DB lookup. ```typescript // Wrong: returns an unresolved Promise as the value transform(value: string) { const exists = this.service.exists(value); // no await! if (exists) throw new BadRequestException(); return value; } // Correct async transform(value: string) { const exists = await this.service.exists(value); if (exists) throw new BadRequestException('Already taken'); return value; } ``` ### Real-world usage - `main.ts` in most NestJS apps: global `ValidationPipe` with `whitelist: true` and `transform: true` - Auth endpoints: `ParseUUIDPipe` on `@Param('userId')` rejects malformed IDs before hitting the DB - Prisma apps: custom pipes validate enum values before passing to Prisma queries - GraphQL resolvers: `ValidationPipe` on `@Args()` works the same way as on REST controllers - NestJS microservices: pipes validate `MessagePattern` payloads using the same `PipeTransform` API ### Follow-up questions **Q:** What is the difference between a global pipe and a route-level pipe? **A:** Global pipes run on every request across all controllers. Route or parameter-level pipes run only where decorated. Use global for universal validation (DTOs), use local for specific type coercion. **Q:** Can a pipe access the full HTTP request object? **A:** Not directly through `transform()` arguments. In a custom pipe you can inject `REQUEST` scope or use `ExecutionContext`. Most use cases do not need it. **Q:** How does `ValidationPipe` handle nested DTOs? **A:** Add `@ValidateNested()` to the nested property and `@Type(() => NestedDto)` from class-transformer. For arrays, add `each: true` on the validator decorator. **Q:** What happens if the first of two pipes on a parameter throws? **A:** Execution stops immediately. The second pipe never runs, the handler never executes, and the exception goes to the exception filter. **Q (Senior):** You need to rate-limit by IP using Redis inside a pipe. What are the pitfalls? **A:** You need request scope to read the IP, either via `Inject(REQUEST)` or `ExecutionContext`. The `transform()` must be `async`. Wrap the Redis call in a try/catch with a fallback; a Redis timeout without it becomes a 500. Test under concurrent load too: without atomic operations (`INCR` + `EXPIRE` in a single transaction), you hit race conditions where multiple requests slip through before the counter increments. ## Examples ### Basic: parsing a URL param ```typescript import { Controller, Get, Param } from '@nestjs/common'; import { ParseIntPipe } from '@nestjs/common'; @Controller('cats') export class CatsController { @Get(':id') findCat(@Param('id', ParseIntPipe) id: number) { // id is always a number here return `Cat #${id}`; } } // GET /cats/5 → "Cat #5" // GET /cats/foo → 400 "Validation failed (numeric string is expected)" ``` `ParseIntPipe` runs before the method body executes. The handler only sees valid integers. ### Intermediate: DTO validation on a POST endpoint ```typescript import { IsEmail, IsInt, Min } from 'class-validator'; import { Body, Controller, Param, Post } from '@nestjs/common'; import { ParseIntPipe, ValidationPipe } from '@nestjs/common'; class UpdateUserDto { @IsEmail() email: string; @IsInt() @Min(18) age: number; } @Controller('users') export class UsersController { @Post(':id') updateUser( @Param('id', ParseIntPipe) id: number, @Body(new ValidationPipe({ transform: true })) dto: UpdateUserDto, ) { // id is number, email is validated, age is number >= 18 return this.usersService.update(id, dto); } } // POST /users/1 { "email": "test@example.com", "age": "20" } // → dto = { email: "test@example.com", age: 20 } // POST /users/1 { "email": "bad", "age": 15 } // → 400 with validation error details ``` The `transform: true` option converts `"20"` (string from JSON) to `20` (number) before `@IsInt()` runs. Without it the check would fail on a string. ### Advanced: custom async pipe for a uniqueness check ```typescript import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'; import { UsersService } from './users.service'; @Injectable() export class UniqueEmailPipe implements PipeTransform<string> { constructor(private readonly usersService: UsersService) {} async transform(value: string, metadata: ArgumentMetadata): Promise<string> { const exists = await this.usersService.emailExists(value); if (exists) { throw new BadRequestException(`Email "${value}" is already registered`); } return value; } } // Usage in controller @Post() async createUser( @Body('email', UniqueEmailPipe) email: string, ) { return this.usersService.create({ email }); } ``` Three things make this work correctly. `UniqueEmailPipe` is `@Injectable()`, so NestJS handles dependency injection and the service is available. `transform()` is `async`, which NestJS awaits before calling the handler. And `BadRequestException` is used, not a plain `Error`, so the response is 400 and not 500.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.