Transform or validate input data before it reaches the route handler.
Signature
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from "@nestjs/common"@Injectable()export class ParseIntPipe implements PipeTransform<string, number> { transform(value: string, metadata: ArgumentMetadata): number { const parsed = parseInt(value, 10) if (isNaN(parsed)) throw new BadRequestException() return parsed }}
Generate with the CLI
nest generate pipe parse-int # full formnest g pi parse-int # short alias → src/parse-int/parse-int.pipe.tsnest g pi parse-int --flat # no wrapping folder → src/parse-int.pipe.tsnest g pi common/trim # nested path → src/common/trim/trim.pipe.tsnest g pi common/trim --flat # nested + flat → src/common/trim.pipe.tsnest g pi parse-int --no-spec # skip the *.spec.ts test filenest g pi parse-int --dry-run # preview the file plan, write nothing
Creates <name>.pipe.ts (and <name>.pipe.spec.ts unless --no-spec). The nest CLI wraps the file in a folder named after the element by default; pass --flat to drop it directly in the target path. Source: @nestjs/cli generate command, Nest CLI usages.
Per-argument scope: a pipe receives one handler argument (@Body(), @Query('id'), …) plus its metatype, not the full Request. That is what makes ValidationPipe automatic: it looks up the DTO class from the metatype and runs class-validator against just that value.
Transform or reject: return a value to pass it on (optionally coerced/sanitized), throw to reject with 400 BadRequestException by default. There is no next(), no response stream, no Observable.
Wrong layer for other jobs: authorization belongs in a guard (boolean decision, 403); wrapping or timing belongs in an interceptor (sees the response); request-shape mutation across many routes belongs in middleware (handler args don’t exist there yet).
Returns default when value is null, undefined, or NaN. See the section below
ParseFilePipe
upload validation
Compose MaxFileSizeValidator + FileTypeValidator directly, or use the fluent ParseFilePipeBuilder. See File uploads recipe
Common options across Parse* pipes
Option
Default
What it does
errorHttpStatusCode
400
Status used when validation fails
exceptionFactory
BadRequestException
Build a custom exception from the error string
optional
false
When true, nil values pass through instead of throw
Binding
Scope
How
Global
app.useGlobalPipes() or the [[nestjs/fundamentals/global-providers|APP_PIPE provider]]
Controller
@UsePipes() on the class
Route
@UsePipes() on the method
Param
@Body(new ValidationPipe())
Pass the class to @UsePipes, not an instance
@UsePipes(MyPipe) is resolved by Nest’s DI container so the pipe’s constructor injections work. @UsePipes(new MyPipe()) skips DI: any injected dependency is undefined and the pipe crashes the first time it touches it. The param-level form @Body(new ValidationPipe({ whitelist: true })) is a deliberate exception — built-in pipes like ValidationPipe take a stateless options object rather than DI-resolved dependencies, so the instance form is idiomatic there. Same trap covered in detail at Guards > Binding.
The global-scope variant of the same DI question — useGlobalPipes(new X()) vs APP_PIPE — has its own dedicated note: Global pipes, guards, interceptors, and filters via DI. It covers the side-by-side comparison, request-scope and hybrid-app implications, and when to reach for useClass vs useFactory.
Order: the param level reversal
Standard order is global, controller, route. But at the route parameter level, pipes run from the last parameter to the first:
import { Body, Controller, Param, Patch, Query, UsePipes } from "@nestjs/common"@UsePipes(GeneralValidationPipe)@Controller("cats")export class CatsController { @UsePipes(RouteSpecificPipe) @Patch(":id") updateCat( @Body() body: UpdateCatDTO, @Param() params: UpdateCatParams, @Query() query: UpdateCatQuery, ) {}}// GeneralValidationPipe runs on: query, then params, then body.// Then RouteSpecificPipe runs in the same reversed order.
DefaultValuePipe
Returns its constructor argument when the incoming value is null, undefined, or NaN. Order matters when chaining:
import { Controller, DefaultValuePipe, Get, ParseIntPipe, Query } from "@nestjs/common"@Controller("cats")export class CatsController { @Get() list( @Query("page", new DefaultValuePipe(1), ParseIntPipe) page: number, @Query("size", new DefaultValuePipe(10), ParseIntPipe) size: number, ) {}}
DefaultValuePipe runs first so ParseIntPipe receives a number, not undefined. Reverse the order and ParseIntPipe would throw on missing query params.
What "missing" means
The default kicks in for null, undefined, and NaN. An empty string (?page=) is not nil, so it passes through and ParseIntPipe will throw. If you need to treat empty strings as missing, normalize upstream (e.g., a custom pipe).
This section is a reference for the option flags. For end-to-end DTO patterns — global setup, whitelist, transform, validation groups, nested objects, custom validators, exceptionFactory — see the recipe.
Install peer deps:
npm i class-validator class-transformer
Built-in options (from ValidationPipeOptions)
Option
Default
What it does
transform
false
Run class-transformer to instantiate DTO classes from plain objects. Required if you want primitives coerced or DTO methods to work
transformOptions
undefined
Forwarded to class-transformer. Common: enableImplicitConversion: true to coerce strings → number/boolean based on TS types
disableErrorMessages
false
Hide validation messages in the response (use in production)
errorHttpStatusCode
400
Status used when validation fails (e.g., set to 422)
exceptionFactory
flattens to BadRequestException
Custom exception shape
validateCustomDecorators
false
Validate args from custom param decorators too
expectedType
undefined
Force the type to validate against (overrides metatype)
Inherited class-validator options (subset)
Option
Default
What it does
whitelist
false
Strip properties without validation decorators
forbidNonWhitelisted
false
Throw instead of stripping
forbidUnknownValues
false
Reject unknown objects. Nest forces false even though class-validator’s own default is true (issue #10683)
With transform: true, the value your handler receives is a DTO class instance, not the raw req.body. If you log/serialize it elsewhere assuming the original shape, you may see unexpected fields stripped (when whitelist is on) or types coerced. This is intentional but easy to miss.
Class vs. instance binding
@UsePipes(ValidationPipe) lets Nest instantiate the pipe (DI works, no options).
@UsePipes(new ValidationPipe({ whitelist: true })) gives you options but loses DI for that instance.
import { PipeTransform, BadRequestException } from "@nestjs/common"import { ZodSchema } from "zod"export class ZodValidationPipe implements PipeTransform { constructor(private schema: ZodSchema) {} transform(value: unknown) { const parsed = this.schema.safeParse(value) if (!parsed.success) throw new BadRequestException(parsed.error.format()) return parsed.data }}
Bind per param: @Body(new ZodValidationPipe(createUserSchema)). Source: zod, Nest docs example.
Common recipes
Trim and normalize string input
Pure transform pipe. No exception path: just clean the value and pass it on.
import { ArgumentMetadata, Injectable, PipeTransform } from "@nestjs/common"@Injectable()export class TrimPipe implements PipeTransform<unknown, unknown> { transform(value: unknown, _metadata: ArgumentMetadata) { if (typeof value === "string") return value.trim() if (value && typeof value === "object") { for (const key of Object.keys(value)) { const v = (value as Record<string, unknown>)[key] if (typeof v === "string") (value as Record<string, unknown>)[key] = v.trim() } } return value }}
Bind globally with app.useGlobalPipes(new TrimPipe(), new ValidationPipe(...)). Order matters: TrimPipe runs first so @IsNotEmpty() sees the trimmed string.
Param to entity lookup (async pipe)
Resolve a route param into a domain entity once, instead of every handler doing the DB call itself. Throws 404 if missing.
import { ArgumentMetadata, Injectable, NotFoundException, PipeTransform } from "@nestjs/common"import { CatsService } from "./cats.service"import { Cat } from "./cat.entity"@Injectable()export class CatByIdPipe implements PipeTransform<string, Promise<Cat>> { constructor(private readonly cats: CatsService) {} async transform(id: string, _metadata: ArgumentMetadata): Promise<Cat> { const cat = await this.cats.findById(id) if (!cat) throw new NotFoundException(`Cat ${id} not found`) return cat }}
import { Controller, Get, Param } from "@nestjs/common"import { CatByIdPipe } from "./cat-by-id.pipe"import { Cat } from "./cat.entity"@Controller("cats")export class CatsController { @Get(":id") getOne(@Param("id", CatByIdPipe) cat: Cat) { return cat }}
The handler receives the entity directly. The pipe is @Injectable(), so Nest wires CatsService automatically. Source: Pipes > Providing defaults.
Compose multiple pipes on the same param
Pipes after the first receive the previous pipe’s output, not the raw value. Use this to default-then-coerce, or coerce-then-validate.
import { Controller, DefaultValuePipe, Get, ParseEnumPipe, ParseIntPipe, Query,} from "@nestjs/common"enum SortOrder { Asc = "asc", Desc = "desc",}@Controller("cats")export class CatsController { @Get() list( @Query("page", new DefaultValuePipe(1), ParseIntPipe) page: number, @Query("order", new DefaultValuePipe(SortOrder.Asc), new ParseEnumPipe(SortOrder)) order: SortOrder, ) {}}
Same param, multiple pipes, evaluated left-to-right. DefaultValuePipe first so downstream pipes never see undefined.
Common errors
Symptom
Likely cause
DTO instance methods are undefined
Missing transform: true — you got a plain object
Numbers arrive as strings
Add transformOptions: { enableImplicitConversion: true } or use @Type(() => Number) from class-transformer
Extra fields appear in DTO
Enable whitelist: true to strip them
Validation always passes
Pipe not bound globally, or DTO class lacks decorators
ParseIntPipe throws on optional param
Either provide a DefaultValuePipe first, or pass { optional: true } to ParseIntPipe
Gotchas
enableImplicitConversion does not handle every type
class-transformer implicit conversion only triggers in plain → class direction, reads Reflect.getMetadata('design:type', ...) (so the property needs at least one decorator), and only knows how to convert String, Number, Boolean, Date, Buffer. Where it works and where it doesn’t:
string, number, boolean, Date: implicit conversion is enough. @Type() not needed.
Branded types (string & { __brand: 'Id' }): converts as the base type (String). The brand is compile-time only, no runtime guarantee — add @IsUUID(), regex, or a custom validator if you care.
Nested class (no circular imports): sometimes works implicitly, but always declare @Type(() => NestedClass) to be safe.
Array of classes (items: Item[]): does not work. TS emits design:type = Array with no element info. @Type(() => Item) is required.
interface / structural type: does not work. TS emits design:type = Object, the value stays as a plain object. Use a real class.
Rule of thumb: implicit conversion is a primitive-coercion shortcut, not a substitute for @Type() on anything object-shaped.
Arrays of classes need both @Type() and @ValidateNested({ each: true })
The Item[] in TypeScript is invisible at runtime — class-transformer reads Array.isArray(value) and applies whatever @Type() says to each element. Without @Type(), elements stay as plain objects. Without @ValidateNested({ each: true }) from class-validator, the decorators inside Item (@IsString(), @IsInt(), etc.) are not executed on the children — silent pass.
import { Type } from "class-transformer"import { ValidateNested, IsString } from "class-validator"class Item { @IsString() name: string}export class CreatePostDto { @ValidateNested({ each: true }) @Type(() => Item) items: Item[]}
What each setup gives you:
Neither decorator → items stays an array of plain objects, children not validated.
@Type(() => Item) alone → items becomes Item instances, but @IsString() inside Item never runs (no @ValidateNested).
@ValidateNested({ each: true }) alone → items stays plain objects, validator has no class to validate against.
Both → items becomes Item instances and their decorators run. ✅
ValidationPipe reads the runtime metatype emitted by TypeScript (Reflect.getMetadata('design:paramtypes', ...)). A type-only import is erased at compile time, so the metatype becomes Object and the pipe falls back to passing the value through untouched: no decorator runs, no error thrown. Always import DTOs as values:
import { CreateUserDto } from "./create-user.dto" // ✅import type { CreateUserDto } from "./create-user.dto" // ❌ validation disabled
TypeScript erases generics and interfaces during compilation, so they leave nothing for class-validator to inspect. ValidationPipe will not validate Partial<CreateCatDto>, Pick<...>, a bare interface, or a union type. Use a concrete class (often via @nestjs/mapped-types helpers like PartialType, PickType, OmitType, IntersectionType). Source: Auto-validation.
body: CreateUserDto[] is not validated as an array of DTOs
@Body() bulk: CreateUserDto[] reaches the pipe with metatype = Array — the element type is gone. The pipe iterates nothing and passes the array through. Two fixes:
import { Body, Controller, ParseArrayPipe, Post } from "@nestjs/common"import { Type } from "class-transformer"import { ValidateNested } from "class-validator"import { CreateUserDto } from "./create-user.dto"// 1. ParseArrayPipe carries the element class explicitly@Controller("users")export class UsersController { @Post("bulk") createBulk( @Body(new ParseArrayPipe({ items: CreateUserDto })) bulk: CreateUserDto[], ) {}}// 2. Wrap in a DTO with @Type()export class CreateUsersDto { @ValidateNested({ each: true }) @Type(() => CreateUserDto) users: CreateUserDto[]}
Pipes run on decorator-extracted arguments (@Body, @Param, @Query, custom decorators). When you grab @Req() or @Res() directly, you’re working with the raw Express/Fastify objects — no metatype, no pipe runs. If you need validation, decorate properties (@Body() body: Dto) instead of reaching into req.body yourself.
When to reach for it
DTO validation with class-validator and ValidationPipe.
String to number or string to UUID coercion.
Trim, lowercase, normalize input shape.
When not to
Authorization decisions: use a guard. Pipes run after guards in the lifecycle and have no concept of “deny this request”.
Mutating the raw request before any handler-level concern: use middleware — pipes only see one argument at a time, not the whole request object.
Wrapping the response or timing the handler: that’s an interceptor. Pipes don’t run on the way out.
Catching a thrown error to reshape it: use an exception filter. A pipe’s job ends at “throw”.