DEV Community

Cover image for NestJS Series - Pipes
Vignesh Pugazhendhi
Vignesh Pugazhendhi

Posted on

NestJS Series - Pipes

Most backend developers can relate the pain of validating and transforming the data passed in the request body or in params or in queries. Most of us either code custom validation and transformation methods or use any open source library. Nestjs supports both through pipes. Pipes in nestjs like any other backend framework have two typical use cases:

1. transformation
2. validation

In transformation, the input data is transformed into a desired form, eg: transforming every string in an array to lowercase or uppercase.

In validation, we validate the input data and simply pass it unchanged if the data is correct and throw an error if the data is incorrect.

Pipes handle both the cases by operating on the arguments being passed to a route controller or a resolver.Nest interposes pipes before any controlled or a resolver method is executed. Pipes receive the arguments passed to the controller and either of validation or transformation code is run on the arguments to decide whether to throw an exception or to transform the data or to pass it unchanged back to the controller's context.

NodeJS developers might worry about exception handling but nestjs takes care of exceptions thrown by pipes. Pipes run only inside the exception zone and all the error handing stuff is taken care by the underlying framework.

Nestjs comes with 8 built in pipes out of which 6 are transformation pipes and 1 is a validation pipe.There is an extra pipe which sets a default value, known as the DefaultValuePipe.Enough of the theory, let's jump into the code:

@Get(':_id')
async findUserById(@

Param('_id',ParseIntPipe) id:number):Promise<UserDto>{
   return this.userService.findById(id);
}

Enter fullscreen mode Exit fullscreen mode

ParseIntPipe transforms the param _id from a string to a number. Notice how we have binded the pipe in the @param decorator.You can also instantiate the pipe yourself and change it's behaviour if needed or let nestjs do it for you:

@Get(':_id')
async findUserById(@Param('_id',new ParseIntPipe( errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE })) id:number):Promise<UserDto>{
   return this.userService.findById(id);
}

Enter fullscreen mode Exit fullscreen mode

As long as your need is minute, you can use the nestjs's built-in transformation pipes.But what if your pipes need to transform or validate based on your project's use cases. Well nest allows us to build our own custom pipes. All we have to do is to implement the PipeTransform class and fulfill the contract of using it's method, transform. This is shown below:

import {PipeTransform,Injectable,ArgumentMetaData} from '@nestjs/common';

@Injectable()
export class CustomPipeTransformation implements PipeTransform{
async transform(value:any,metadata:ArgumentMetaData){
return value;
}
}
Enter fullscreen mode Exit fullscreen mode

Here value is the data you pass in the request body or as params or as queries. metadata of type ArgumentMetaData contains the metadata associated with the data you pass.

This is an object with 3 keys, data,type and metatype.

export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}
Enter fullscreen mode Exit fullscreen mode

type denotes how you pass the data, either in body or in params or in queries.
metatype is the type of the data you send, eg:string
data is the name you pass to the decorator. eg: @Body('email')

These 3 keys and the value argument can enough to transform or validate your data. Following code snippet converts an argument of array of strings to array of uppercase strings.That is each element in the array is transformed to it's uppercase version.

import {
  PipeTransform,
  Injectable,
  ArgumentMetadata,
  BadRequestException,
} from '@nestjs/common';
import { string } from 'joi';

@Injectable()
export class PipeTransformCustom implements PipeTransform<any> {
  async transform(value: any, { data, type, metatype }: ArgumentMetadata) {
    if (this.isTypeAcceptable(metatype, value) && type==='body') {
      return (value as string[]).map((val: string) => val.toLocaleUpperCase());
    }
    throw new BadRequestException(
      `Argument expected should be an array of strings!`,
    );
  }
  isTypeAcceptable(type: any, value: any): boolean {
    if (typeof type === 'function' && Array.isArray(value)) {
      return value.every((val) => typeof val === 'string');
    }
    return false;
  }
}
Enter fullscreen mode Exit fullscreen mode

Note how we destructure the metadata and make use of it's keys. First the metatype is being checked if it is of type "function" and value being checked if it is an array. Once this is done, we check if every element in the array is of type "string". Once both the cases pass, we convert every element of the array to it's locale uppercase. This example is a combination of both validation and transformation.

To make use of this pipe,

@Controller('convert-upper-case')
@UsePipes(PipeTransformCustom)
async convertToUppercase(@Body('array') array:string[]):string[]>{
return Promise.resolve(()=>array);
}
Enter fullscreen mode Exit fullscreen mode

There's another type of validation known as schema-based validation which validates your request body data against a schema. By schema I don't mean an actual DB schema,it may be an interface or class.

This is explained in the following code:

export class UserSignupDto{
email:string;
username:string;
phone:number;
above18:boolean;
}
Enter fullscreen mode Exit fullscreen mode

Now to validate any incoming data against this schema, we have two options: either we can do it manually as explained above or we can make use of libraries. The Joi library allows you to create schemas in a straightforward way, with a readable API

npm i joi --save
npm i @types/joi --save-dev
Enter fullscreen mode Exit fullscreen mode

We install types support for Joi as a dev dependency.Now to utilize this lib,

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ObjectSchema } from 'joi';

@Injectable()
export class JoiValidationPipe implements PipeTransform {
  constructor(private schema: ObjectSchema) {}

  transform(value: any, metadata: ArgumentMetadata) {
    const { error } = this.schema.validate(value);
    if (error) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }
}

@Post()
@UsePipes(new JoiValidationPipe(UserSignupDto))
async create(@Body() createUserDto: UsersignupDto) {
  this.userService.create(createUserDto);
}
Enter fullscreen mode Exit fullscreen mode

here we make use of Joi lib to validate the data against the UserSignupDto. The code is pretty starightforward and can be understood easily. That's how Joi makes validation look easy.

An added advantage of nest is it works well with class-validator library. This powerful library allows you to use decorator-based validation. Decorator-based validation is extremely powerful, especially when combined with Nest's Pipe capabilities since we have access to the metatype of the processed property.

npm i --save class-validator class-transformer
Enter fullscreen mode Exit fullscreen mode

Once installed we can add decorators to our UserSignupDto schema.

import {IsString,IsBoolean,IsNumber} from 'class-validator';

export class UserSignupDto{
@IsString()
email:string;
@IsString()
username:string;
@IsNumber()
phone:number;
@IsBoolean()
above18:boolean;
}

Enter fullscreen mode Exit fullscreen mode

If needed, you can read about class-validator here class-validator

You can also bind pipes at different contexts of the code- be it an controller level, module level or at global level.

I'm attaching the code snippets from official docs for module and global level. Please refer to the docs if an in-depth knowledge is needed.

Module level

import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: ValidationPipe,
    },
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Global Level

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

Enter fullscreen mode Exit fullscreen mode

Pipes to some extent make the code less vulnerable to production bugs.

Top comments (0)