title: 'Pipes · Nael Framework' description: 'Learn how to use pipes for transforming and validating input data before it reaches route handlers in Nael applications.'
Pipes
A pipe is a class annotated with the PipeTransform interface. Pipes have two typical use cases:
transformation (transform input data to the desired form) and validation
(evaluate input data and if valid, pass it through unchanged; otherwise, throw an exception).
Creating a pipe
Pipes implement the PipeTransform<T, R> interface, which requires a transform() method.
This method takes two parameters: the value to transform and metadata about the argument.
import { PipeTransform, ArgumentMetadata } from '@nl-framework/http';export class TrimPipe implements PipeTransform<string, string> { transform(value: string, metadata: ArgumentMetadata): string { return typeof value === 'string' ? value.trim() : value; }}The transform() method can be synchronous or asynchronous. It should return the transformed value
or throw an exception if validation fails.
Using pipes
Pipes can be applied directly to route handler parameters. Simply pass the pipe class or instance after the parameter decorator:
import { Controller, Get, Query } from '@nl-framework/http';import { UsePipes } from '@nl-framework/core';@Controller('/search')export class SearchController {@Get('/')search(@Query('q', TrimPipe) query: string) {return { query };}}Built-in pipes
Nael provides several built-in pipes for common transformation tasks:
import { Controller, Get, Param, ParseIntPipe } from '@nl-framework/http';@Controller('/users')export class UsersController {@Get('/:id')findOne(@Param('id', ParseIntPipe) id: number) {// id is automatically converted to a numberreturn { id, type: typeof id }; // { id: 123, type: 'number' }}}All built-in pipes are exported from @nl-framework/http:
import { ParseIntPipe, ParseFloatPipe, ParseBoolPipe, ParseArrayPipe, DefaultValuePipe,} from '@nl-framework/http';@Controller('/products')export class ProductsController {@Get('/')list(@Query('page', ParseIntPipe) page: number,@Query('limit', ParseIntPipe) limit: number,@Query('active', ParseBoolPipe) active: boolean,@Query('tags', new ParseArrayPipe({ separator: ',' })) tags: string[],@Query('sort', new DefaultValuePipe('name')) sort: string,) {return { page, limit, active, tags, sort };}}ParseIntPipe- Converts string to integerParseFloatPipe- Converts string to floatParseBoolPipe- Converts string to booleanParseArrayPipe- Splits string into arrayDefaultValuePipe- Provides default value for undefined/null
Validation pipe
The ValidationPipe uses class-validator and class-transformer to validate
and transform input based on DTO class decorators:
import { Controller, Post, Body, ValidationPipe } from '@nl-framework/http';import { IsEmail, IsString, MinLength } from 'class-validator';class CreateUserDto {@IsEmail()email!: string;@IsString()@MinLength(3)name!: string;}@Controller('/users')export class UsersController {@Post('/')create(@Body(ValidationPipe) data: CreateUserDto) {// data is validated and transformedreturn { message: 'User created', data };}}If validation fails, the pipe throws an HttpException with a 400 status code.
Global pipes
Use @UsePipes() at the class level to apply pipes to all handler methods. Import the decorator from
@nl-framework/core so HTTP, GraphQL, and microservice handlers share identical metadata (each package also
re-exports it for backwards compatibility):
import { Controller, Post, Body, ValidationPipe } from '@nl-framework/http';import { UsePipes } from '@nl-framework/core';@Controller('/users')@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))export class UsersController {@Post('/')create(@Body() data: CreateUserDto) {// ValidationPipe is applied to all parametersreturn { message: 'User created', data };}@Post('/bulk')createMany(@Body() data: CreateUserDto[]) {// ValidationPipe is applied here tooreturn { message: 'Users created', count: data.length };}}Method-level pipes
Apply @UsePipes() at the method level for more granular control:
import { Controller, Post, ValidationPipe } from '@nl-framework/http';import { UsePipes } from '@nl-framework/core';@Controller('/products')export class ProductsController {@Post('/')@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))create(@Body() data: CreateProductDto) {return { message: 'Product created', data };}@Post('/import')@UsePipes(new ValidationPipe({ skipMissingProperties: true }))import(@Body() data: ImportProductDto[]) {return { message: 'Products imported', count: data.length };}}Custom validation pipes
Create custom pipes for specialized validation logic:
import { PipeTransform, ArgumentMetadata, HttpException } from '@nl-framework/http';export class FileSizeValidationPipe implements PipeTransform { constructor(private readonly maxSize: number) {}transform(value: any, metadata: ArgumentMetadata): any {if (value?.size && value.size > this.maxSize) {throw HttpException.badRequest(`File size exceeds maximum of ${this.maxSize} bytes`);}return value;}}@Controller('/upload')export class UploadController {@Post('/')upload(@Body(new FileSizeValidationPipe(1024 * 1024)) file: any) {return { message: 'File uploaded', size: file.size };}}Transformation pipes
Pipes can transform input data before it reaches the handler:
import { PipeTransform, ArgumentMetadata } from '@nl-framework/http';export class UpperCasePipe implements PipeTransform<string, string> { transform(value: string, metadata: ArgumentMetadata): string { return value?.toUpperCase() || value; }}export class SlugifyPipe implements PipeTransform<string, string> { transform(value: string, metadata: ArgumentMetadata): string { return value .toLowerCase() .trim() .replace(/[^\w\s-]/g, '') .replace(/[\s_-]+/g, '-') .replace(/^-+|-+$/g, ''); }}@Controller('/posts')export class PostsController {@Post('/')create(@Body('title', SlugifyPipe) slug: string,@Body('name', UpperCasePipe) name: string,) {return { slug, name };}}Async pipes
Pipes can be asynchronous, making them suitable for database lookups or API calls:
import { PipeTransform, ArgumentMetadata, HttpException } from '@nl-framework/http';export class UserExistsPipe implements PipeTransform { async transform(userId: string, metadata: ArgumentMetadata): Promise<string> { const user = await this.findUser(userId); if (!user) { throw HttpException.notFound(`User with ID ${userId} not found`); } return userId; }private async findUser(id: string) {// Query databasereturn null;}}@Controller('/posts')export class PostsController {@Post('/')create(@Body('userId', UserExistsPipe) userId: string,@Body('title') title: string,) {return { userId, title };}}Parsing arrays
ParseArrayPipe can split strings and transform individual items:
import { Controller, Get, Query, ParseArrayPipe, ParseIntPipe } from '@nl-framework/http';@Controller('/products')export class ProductsController {@Get('/filter')filter(// Parse comma-separated string into array@Query('tags', new ParseArrayPipe({ separator: ',' })) tags: string[], // Parse comma-separated numbers @Query( 'ids', new ParseArrayPipe({ separator: ',', items: new ParseIntPipe() }) ) ids: number[],) {return { tags, ids };}}Pipe chaining
Multiple pipes can be applied to a single parameter. They execute in order:
import { Controller, Get, Query, DefaultValuePipe, ParseIntPipe } from '@nl-framework/http';@Controller('/products')export class ProductsController {@Get('/')list(// First DefaultValuePipe, then ParseIntPipe@Query('page', new DefaultValuePipe('1'), ParseIntPipe) page: number,) {return { page };}}Note: Pipes are executed in the order they are specified. The output of one pipe becomes the input of the next.
Validation pipe options
The ValidationPipe accepts various options to customize its behavior:
import { ValidationPipe } from '@nl-framework/http';// Basic validationnew ValidationPipe()// Transform and validatenew ValidationPipe({ transform: true })// Strip unknown propertiesnew ValidationPipe({ whitelist: true })// Throw error on unknown propertiesnew ValidationPipe({ forbidNonWhitelisted: true })// Skip missing properties during validationnew ValidationPipe({ skipMissingProperties: true })// Validate specific groupsnew ValidationPipe({ groups: ['create'] })// Custom exception factorynew ValidationPipe({exceptionFactory: (errors) => {return HttpException.unprocessableEntity(`Validation failed: ${errors.length} errors`);},})Dependency injection
Pipes can use dependency injection when registered as providers:
import { Injectable } from '@nl-framework/core';import { PipeTransform, ArgumentMetadata, HttpException } from '@nl-framework/http';@Injectable()export class EntityExistsPipe implements PipeTransform {constructor(private readonly db: DatabaseService) {}async transform(id: string, metadata: ArgumentMetadata): Promise<string> {const exists = await this.db.exists(id); if (!exists) { throw HttpException.notFound(`Entity ${id} not found`); } return id;}}// Register as a provider@Module({providers: [EntityExistsPipe, DatabaseService],controllers: [EntityController],})export class EntityModule {}Important: Pipes must be registered as providers in a module to use dependency injection. Otherwise, they are instantiated directly and dependencies won't be resolved.
Execution order
Understanding pipe execution order helps build robust data processing pipelines:
- Controller-level pipes (from
@UsePipes()on class) - Method-level pipes (from
@UsePipes()on method) - Parameter-level pipes (from parameter decorator)
- For multiple pipes on same parameter, they execute left-to-right
- If any pipe throws, subsequent pipes are skipped and exception is caught
Best practices
Use built-in pipes first - They handle common cases efficiently
Keep pipes focused - Each pipe should do one thing well
Validate early - Use pipes to validate at the entry point
Provide clear errors - Throw meaningful exceptions with context
Consider performance - Async pipes add latency, use sparingly
Make pipes reusable - Design for use across multiple endpoints
Document expectations - Clearly specify what pipes transform/validate
What's next?
Learn about Guards for authentication and authorization, or explore Interceptors for request/response transformation.