Pipes
Pipes transform and validate incoming request data before it reaches your controller methods. They are executed after middleware and before your route controllers, and operate on individual route parameters.
Overview
YasuiJS pipes can be useful for:
- Validation - Check if incoming data meets expected criteria
- Transformation - Convert data to desired formats or types
- Sanitization - Clean and normalize input data
Pipes can be applied at three levels:
- Global level - Applied to all parameters in your application
- Controller level - Applied to all parameters in a controller
- Method level - Applied to parameters in specific route methods
import { PipeTransform, IParamMetadata } from 'yasui';
@PipeTransform()
export class ValidationPipe implements IPipeTransform {
transform(value: any, metadata: IParamMetadata) {
// Validation logic here
return value;
}
}
Creating Pipes
Pipe Interface
All pipes must implement the IPipeTransform
interface with a single transform
method:
import { PipeTransform, IPipeTransform, IParamMetadata } from 'yasui';
@PipeTransform()
export class ParseIntPipe implements IPipeTransform {
transform(value: any, metadata: IParamMetadata): number {
const parsed = parseInt(value, 10);
if (isNaN(parsed)) {
throw new HttpError(400, `Expected number, got "${value}"`);
}
return parsed;
}
}
IParamMetadata
The transform
method receives metadata about the parameter being processed:
type
: Source of the parameter (body, query, param, headers etc)name?
: Property name in the request source object */metatype?
: Underlying type of the parameter, based on the type definition in the route handler
See the example in the Integration with Class Validator section to see the metadata in use.
Pipes receive typed values after automatic type casting.
Using Pipes
Method Level
Apply pipes to specific route methods using @UsePipes()
:
@Controller('/users')
export class UserController {
@Get('/:id')
@UsePipes(ParseIntPipe)
getUser(@Param('id') id: number) {
// id is guaranteed to be a number
return this.userService.findById(id);
}
}
Controller Level
Apply pipes to all methods in a controller:
@Controller('/users')
@UsePipes(ValidationPipe, LoggingPipe)
export class UserController {
@Post('/')
createUser(@Body() createUserDto: CreateUserDto) {
// All parameters validated and logged for all routes
}
}
Global Level
Apply pipes to all parameters across your entire application:
yasui.createServer({
controllers: [UserController],
globalPipes: [ValidationPipe, TrimPipe]
});
Execution Order
Pipes execute in this order:
- Global pipes (in registration order)
- Controller pipes (in declaration order)
- Method pipes (in declaration order)
// Configuration
yasui.createServer({
globalPipes: [GlobalPipe] // 1. First
});
@Controller('/users')
@UsePipes(ControllerPipe) // 2. Second
export class UserController {
@Post('/')
@UsePipes(MethodPipe) // 3. Third
createUser(@Body() data: any) {
// data has been processed by all three pipes
}
}
Error Handling
Pipes can throw errors to reject invalid requests, as at all other levels, these will be automatically caught by Yasui:
@PipeTransform()
export class RequiredPipe implements IPipeTransform {
transform(value: any, metadata: IParamMetadata) {
if (value === undefined || value === null || value === '') {
const paramName = metadata.name || metadata.type;
throw new HttpError(HttpCode.BAD_REQUEST, `${paramName} is required`);
}
return value;
}
}
Integration with Class Validator
YasuiJS pipes can work seamlessly with class-validator and class-transformer:
See the complete example
import { validate, IsEmail, IsString, MinLength } from 'class-validator';
import { plainToInstance } from 'class-transformer';
import { PipeTransform, IPipeTransform, ParamMetadata, HttpError } from 'yasui';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(3)
name: string;
}
@Controller('/users')
export class UserController {
@Post('/')
@UsePipes(ValidationPipe) // Uses class-validator decorators
createUser(@Body() createUserDto: CreateUserDto) {
// createUserDto is validated and typed
return this.userService.create(createUserDto);
}
}
@PipeTransform()
export class ValidationPipe implements IPipeTransform {
async transform(value: any, metadata: ParamMetadata) {
if (metadata.type !== 'body' && metadata.type !== 'query') {
return value;
}
// Skip validation for primitive types
if (!metadata.metatype || this.isPrimitiveType(metadata.metatype)) {
return value;
}
const object = plainToInstance(metadata.metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
const messages = errors.map(err =>
Object.values(err.constraints || {}).join(', ')
).join('; ');
throw new HttpError(400, `Validation failed: ${messages}`);
}
return object;
}
private isPrimitiveType(type: Function): boolean {
return [String, Boolean, Number, Array, Object].includes(type);
}
}