Pipes
Pipes are classes that can transform or validate incoming data before it reaches the route handler. They are particularly useful for handling Data Transfer Objects (DTOs) from request bodies, parameters, or queries.
A pipe must implement the IPipe interface, which has a single transform method.
interface IPipe<T = any> {
transform(value: T, metadata: ArgumentMetadata): any | Promise<any>
}value: The incoming data from the request.metadata: An object containing information about the parameter being processed, such as its type and the decorator used.
Use Cases
Pipes have two main use cases:
- Transformation: Converting data from one form to another (e.g., converting a string ID to a number).
- Validation: Checking if incoming data meets certain criteria and throwing an exception if it does not.
Official Pipes
HonestJS provides two pipe packages:
@honestjs/pipes–PrimitiveValidationPipefor automatic coercion of path/query params toString,Number, orBoolean.@honestjs/class-validator-pipe–ClassValidatorPipefor validating and transforming DTOs with class-validator and class-transformer.
PrimitiveValidationPipe
Use for path and query parameters. When you declare @Param('id') id: number or @Query('page') page: number, the pipe transforms string values to the expected primitive type.
bun add @honestjs/pipesimport { Application } from 'honestjs'
import { PrimitiveValidationPipe } from '@honestjs/pipes'
const { hono } = await Application.create(AppModule, {
components: {
pipes: [new PrimitiveValidationPipe()]
}
})@Controller('/users')
export class UsersController {
@Get('/:id')
async findOne(@Param('id') id: number) {
// `id` is a number here (e.g. from /users/42)
return this.usersService.findById(id)
}
@Get()
async list(@Query('page') page: number, @Query('limit') limit: number) {
// `page` and `limit` are numbers (e.g. ?page=1&limit=10)
return this.usersService.findAll(page, limit)
}
}ClassValidatorPipe
Use for request bodies and other DTOs. It validates plain objects against class-validator decorators and transforms them into class instances.
bun add @honestjs/class-validator-pipe class-validator class-transformerimport { Application } from 'honestjs'
import { ClassValidatorPipe } from '@honestjs/class-validator-pipe'
const { hono } = await Application.create(AppModule, {
components: {
pipes: [new ClassValidatorPipe()]
}
})import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator'
export class CreateUserDto {
@IsString()
@MinLength(2)
name: string
@IsEmail()
email: string
@IsString()
@IsOptional()
bio?: string
}
@Controller('/users')
export class UsersController {
@Post()
async create(@Body() body: CreateUserDto) {
// `body` is validated and transformed into a CreateUserDto instance
return this.usersService.create(body)
}
}Creating a Custom Pipe
Transformation Example
Here is a simple custom pipe that transforms a string value into a number. You can register it globally or with @UsePipes().
import { IPipe, ArgumentMetadata } from 'honestjs'
import { BadRequestException } from 'http-essentials'
export class ParseIntPipe implements IPipe<string> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10)
if (isNaN(val)) {
throw new BadRequestException('Validation failed: not a number')
}
return val
}
}Validation Example
A custom validation pipe using class-validator and class-transformer. For production, prefer @honestjs/class-validator-pipe which handles edge cases and options.
import { IPipe, ArgumentMetadata } from 'honestjs'
import { plainToInstance } from 'class-transformer'
import { validate } from 'class-validator'
import { BadRequestException } from 'http-essentials'
export class ValidationPipe implements IPipe {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value
}
const object = plainToInstance(metatype, value)
const errors = await validate(object)
if (errors.length > 0) {
throw new BadRequestException('Validation failed', { details: errors })
}
return object
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object]
return !types.includes(metatype)
}
}Applying Pipes
Pipes are applied at the global, controller, or handler level. HonestJS does not support parameter-level pipes (e.g. @Param('id', SomePipe)). Use global or scoped pipes instead.
Global Pipes
Global pipes run on all parameters for all routes. Register them when creating the application.
import { Application } from 'honestjs'
import { PrimitiveValidationPipe } from '@honestjs/pipes'
import { ClassValidatorPipe } from '@honestjs/class-validator-pipe'
const { hono } = await Application.create(AppModule, {
components: {
pipes: [new PrimitiveValidationPipe(), new ClassValidatorPipe()]
}
})Controller and Handler Pipes
Use @UsePipes() to apply pipes to a controller or a specific handler.
import { Body, Controller, Get, Param, UsePipes } from 'honestjs'
import { PrimitiveValidationPipe } from '@honestjs/pipes'
@Controller('/users')
@UsePipes(new PrimitiveValidationPipe())
export class UsersController {
@Get('/:id')
async findOne(@Param('id') id: number) {
return this.usersService.findById(id)
}
}Execution Order
When multiple pipes are applied, they are executed in the following order:
- Global Pipes
- Controller-Level Pipes
- Handler-Level Pipes
If multiple pipes are applied at the same level (e.g. @UsePipes(PipeA, PipeB)), they run in the order listed. Each pipe's output becomes the next pipe's input.
Pipes are a powerful tool for creating robust and type-safe APIs, reducing boilerplate code in route handlers.
