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.
Creating a Pipe
Transformation Example
Here is a simple pipe that transforms a string value into a number.
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 more common use case is validating an incoming request body against a DTO class. This is often done with libraries like class-validator
and class-transformer
.
Here is an example of a ValidationPipe
:
import { IPipe, ArgumentMetadata } from 'honestjs'
import { plainToClass } 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 = plainToClass(metatype, value)
const errors = await validate(object)
if (errors.length > 0) {
throw new BadRequestException('Validation failed', { details: errors })
}
return value
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object]
return !types.includes(metatype)
}
}
This pipe:
- Checks if the parameter has a specific DTO type.
- Uses
class-transformer
to convert the plain JavaScript object from the request into an instance of the DTO class. - Uses
class-validator
to validate the object based on the decorators in the DTO. - Throws an exception if validation fails.
Applying Pipes
Pipes can be applied at the global, controller, or handler level using the @UsePipes()
decorator. They can also be applied to a specific parameter.
Global Pipes
Global pipes are useful for applying validation to all incoming data. A global ValidationPipe
can be set to ensure all DTOs are validated automatically.
const { hono } = await Application.create(AppModule, {
components: {
pipes: [new ValidationPipe()],
},
})
Parameter-Level Pipes
You can also apply pipes to a specific parameter within a route handler.
import { Body, Param, ParseIntPipe } from 'honestjs'
@Controller('/users')
export class UsersController {
@Get('/:id')
async findOne(@Param('id', ParseIntPipe) id: number) {
// The `id` will be a number here, not a string.
return this.usersService.findById(id)
}
}
In this example, ParseIntPipe
is applied only to the id
parameter.
Execution Order
When multiple pipes are applied, they are executed in the following order:
- Global Pipes
- Controller-Level Pipes
- Handler-Level Pipes
- Parameter-Level Pipes
If multiple pipes are applied at the same level (e.g., @UsePipes(PipeA, PipeB)
), they are executed in the order they are 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.