How to Implement Custom Pipes for Request Validation in NestJS
Implement custom pipes in NestJS by creating a class that implements the PipeTransform interface, decorating it with @Injectable(), and defining a transform() method that validates incoming request data and throws exceptions for invalid payloads before they reach the route handler.
Custom pipes are essential building blocks in the nestjs/nest ecosystem for enforcing request validation logic. They intercept incoming data at the boundary of your application, allowing you to sanitize inputs, verify business rules, and prevent invalid requests from ever reaching your controllers. Whether you need simple type coercion or complex async database lookups, implementing the PipeTransform interface gives you full control over request preprocessing.
Understanding the Pipe Architecture
NestJS treats pipes as request-processing units that execute in a specific sequence before your route handler runs. According to the source code in packages/common/interfaces/pipes/pipe-transform.interface.ts, every pipe must implement a single transform(value: any, metadata: ArgumentMetadata) method that receives the raw value and metadata describing the route parameter.
When a request enters the system, Nest constructs a processing pipeline that runs in this order:
- Global pipes (registered in
main.ts) - Controller-level pipes (applied via
@UsePipes()) - Method-level pipes (passed to parameter decorators)
- Route handler
If any pipe throws an exception—typically a BadRequestException—the request stops immediately and the error response returns to the client. The built-in ValidationPipe located at packages/common/pipes/validation.pipe.ts demonstrates this pattern using class-validator and class-transformer, while simpler examples like ParseIntPipe in packages/common/pipes/parse-int.pipe.ts show lightweight type transformation.
Why Create Custom Validation Pipes?
While NestJS provides robust built-in validation via ValidationPipe, custom pipes solve specific architectural needs:
- Domain-specific validation that exceeds decorator capabilities, such as checking database uniqueness or consulting external APIs
- Reusable business logic you can apply across multiple routes without duplicating controller code
- Performance optimization through lightweight synchronous checks that run before heavier class-validator logic
- Dependency injection for async operations, as shown in
integration/scopes/src/transient/users/user-by-id.pipe.ts
Implementing a Synchronous Custom Validation Pipe
Create a basic validation pipe by implementing PipeTransform with a concrete type. This example validates that a route parameter is a positive integer:
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from '@nestjs/common';
@Injectable()
export class PositiveIntPipe implements PipeTransform<number> {
transform(value: any, metadata: ArgumentMetadata) {
const val = parseInt(value, 10);
if (isNaN(val) || val <= 0) {
throw new BadRequestException(
`${metadata.metatype?.name} must be a positive integer`,
);
}
return val;
}
}
Apply this pipe at the method level to validate parameters before they enter your handler:
@Get('items/:limit')
findMany(@Param('limit', PositiveIntPipe) limit: number) {
// `limit` is guaranteed to be a positive integer here
}
Implementing Async Custom Pipes with Dependency Injection
Custom pipes fully support NestJS's dependency injection system, enabling async validation that queries databases or external services. The UserByIdPipe in the integration tests demonstrates this pattern, and you can replicate it for email uniqueness checks:
import {
PipeTransform,
Injectable,
ArgumentMetadata,
ConflictException,
} from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class UniqueEmailPipe implements PipeTransform<string> {
constructor(private readonly usersService: UsersService) {}
async transform(value: string, metadata: ArgumentMetadata) {
const exists = await this.usersService.findByEmail(value);
if (exists) {
throw new ConflictException('Email already in use');
}
return value;
}
}
Because the pipe is @Injectable(), Nest resolves the UsersService dependency automatically, letting you perform async business validation before the controller method executes.
Registering and Applying Custom Pipes
You can bind pipes at three different scopes depending on your validation needs.
Method-level binding targets specific parameters using the @Body(), @Query(), or @Param() decorators:
@Post('users')
async create(@Body(UniqueEmailPipe) email: string) {
// Email is guaranteed unique
}
Controller-level binding applies pipes to every route in a controller using @UsePipes():
@Controller('users')
@UsePipes(PositiveIntPipe)
export class UsersController {
// All routes inherit this pipe logic
}
Global registration in your main.ts ensures all routes use the same validation strategy:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
Combining Built-in and Custom Pipes
NestJS allows you to chain multiple pipes for layered validation. For example, use the built-in ValidationPipe to enforce DTO structure, then apply a custom pipe for business-rule validation:
@Post('register')
async register(
@Body(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
createUserDto: CreateUserDto,
@Body(UniqueEmailPipe) _: any,
) {
// DTO is structurally valid AND email is unique
}
The cats-app sample in sample/01-cats-app/src/common/pipes/parse-int.pipe.ts provides a reference implementation of this pattern, demonstrating how to throw BadRequestException with descriptive messages when validation fails.
Summary
- Implement
PipeTransform: All custom pipes must define atransform(value, metadata)method and be decorated with@Injectable(). - Source files: Study
packages/common/interfaces/pipes/pipe-transform.interface.tsfor the contract andpackages/common/pipes/validation.pipe.tsfor complex validation patterns. - Dependency injection: Constructor parameters in pipes are automatically resolved by Nest, enabling async database checks as shown in
integration/scopes/src/transient/users/user-by-id.pipe.ts. - Application scopes: Bind pipes globally, at the controller level, or to individual parameters using
useGlobalPipes(),@UsePipes(), or parameter decorators. - Error handling: Throw
BadRequestExceptionor domain-specific exceptions to halt request processing when validation fails.
Frequently Asked Questions
How do I access the request object inside a custom pipe?
You cannot directly access the request object inside a pipe. The PipeTransform interface only receives the value being processed and ArgumentMetadata. If you need request-level data (headers, IP address), use a Guard or Interceptor instead, or pass the required data through decorators and the metadata argument.
Can I use multiple pipes on a single parameter?
Yes, NestJS processes pipes left-to-right when declared in an array. For example, @Param('id', ParseIntPipe, PositiveIntPipe) first converts the string to an integer, then validates it is positive. Each pipe receives the output of the previous pipe in the chain.
What is the difference between ValidationPipe and a custom validation pipe?
ValidationPipe is a generic, class-validator-based implementation located at packages/common/pipes/validation.pipe.ts that validates DTO objects using decorators like @IsString(). Custom pipes implement specific business logic that decorators cannot express, such as checking database uniqueness or transforming data using custom algorithms. Use ValidationPipe for structural validation and custom pipes for domain-specific rules.
How do I test a custom pipe in isolation?
Unit test pipes by instantiating them directly and calling the transform() method with mock values and metadata. Since pipes are plain classes with a single method, you do not need to bootstrap an entire NestJS application. Mock any injected dependencies in the constructor to test async validation logic without database connections.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →