title: 'Pipes · Nael Framework' description: 'Learn how to use pipes for transforming and validating input data before it reaches route handlers in Nael applications.'

Overview

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.

trim.pipe.ts
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:

search.controller.ts
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:

ParseIntPipe example
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:

Using built-in pipes
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 integer
  • ParseFloatPipe - Converts string to float
  • ParseBoolPipe - Converts string to boolean
  • ParseArrayPipe - Splits string into array
  • DefaultValuePipe - 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:

Using ValidationPipe
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):

Controller-level pipes
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:

Method-level pipes
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:

Custom validation pipe
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:

Transformation pipes
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:

Async pipe
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:

ParseArrayPipe examples
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:

Chaining pipes
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:

ValidationPipe options
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:

Pipe with dependency injection
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:

  1. Controller-level pipes (from @UsePipes() on class)
  2. Method-level pipes (from @UsePipes() on method)
  3. Parameter-level pipes (from parameter decorator)
  4. For multiple pipes on same parameter, they execute left-to-right
  5. 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.