Nest.JS: CUSTOM VALIDATION PIPE

Nest.JS: CUSTOM VALIDATION PIPE

Nest.js is a progressive node.js framework for building serve-side applications. Under the hood, it uses the Express framework and can also be configured to use Fastify. It is built with and fully supports Typescript (one of the reasons why I love this framework).

Nest.js like Angular provides us with Pipes.

Pipes are basically classes or functions that can take input data, transform it and output the transformed data.

Pipes in nest.js are annotated with the @ injectable() decorator. Typically in all applications, pipes can be used for the purposes of Transformation and Validation. In this article, I will be writing about the validation use case of a pipe.

Say we are building an application that exposes a POST endpoint to create new item in a shopping list and you need the user to provide the following as payload to the endpoint:

  • name (required)
  • description(required)
  • quantity(optional)
  • unit price(required)

Firstly, we need to create a controller that handle the request and we need to specify that the controller should expect a body object as data from the request. The data is typed as "any" (for now).

@Post('/create')
createItem(@Body() data: any) {

}

The next thing is that we need to validate the payload to ensure that it conforms with what the endpoint expects. To do this, we need to install these two packages: class-validator and class-transformer like so;

npm i class-validator class-transformer

Now we need to create a DTO(Data Transfer Object), more like a model that describe how a data should look like, we call it ItemDTO. Create a new file called item.dto.ts

import { IsNotEmpty } from 'class-validator'

export class ItemDTO {
  @IsNotEmpty()
  name: string;

  @IsNotEmpty()
  description: string;

  @IsNotEmpty()
  unitPrice: number;

  quantity: number;
}

The class-validator library provides us with several decorators that can be used to describe a data type. From the snippet above:

  1. name is declared as a string, the IsNotEmpty() decorator ensures that it is a required field.

  2. The same goes for description

  3. unitPrice is expected to be a number and also required

  4. Notice quantity has no decorator, it's however declared as a number;

Now we need to change the type of the payload on our controller to be of type ItemDTO like so;

import { ItemDTO } from './item.dto';

@Post('/create')
createItem(@Body() data: ItemDTO) {

 }

So, what we have done so far is to inform our controller that user is expected to send a payload of type itemDTO, however what happens if the endpoint does not get any payload or probably there is a payload and the payload does not conform with the expected data types, we won't want our application to crash, do we? We will also have to inform the user of the right data types as a guide. This is where our Custom validator comes in. Nest.js has provided us with an in-built Validator pipe. You can read about it here.

We will need to create a new file validation.pipe.ts. we should create a new class CustomValidationPipe, this class should implement the PipeTransform interface which will make us provide a transform method. The transform method takes in parameters like value (which is our payload), metaData which shows more metadata about our payload. Paste the code below in the file.

import { ArgumentMetadata, Injectable, PipeTransform } from "@nestjs/common";

@Injectable()
export class CustomValidationPipe implements PipeTransform {
   transform(value: any, metaData: ArgumentMetadata){

    }
}
  • We need to check that our payload is not empty
import { ArgumentMetadata, HttpException, HttpStatus, Injectable, PipeTransform } from "@nestjs/common";


@Injectable()
export class CustomValidationPipe implements PipeTransform {
   transform(value: any, metaData: ArgumentMetadata){
        if(this.isEmpty(value)) {
           throw new HttpException(`Validation failed: No payload provided`, HttpStatus.BAD_REQUEST)
        }

       private isEmpty(value:any) {
           if(Object.keys(value).length < 1) {
                return true
             }
           return false
        }
    }
}

Notice how HttpStatus was used. Nest.js has an enum of different http statuses that can be leverage upon so you don't have to set status codes yourself. At this point, we are sure we will have a payload.

  • We need to validate the payload against the registered DTO.
import { ArgumentMetadata, HttpException, HttpStatus, Injectable, PipeTransform } from "@nestjs/common";
import { plainToClass } from "class-transformer";
import { validate } from "class-validator";

@Injectable()
export class CustomValidationPipe implements PipeTransform {
   transform(value: any, metaData: ArgumentMetadata){
        const { metatype } = metaData;
        if(this.isEmpty(value)) {
           throw new HttpException(`Validation failed: No payload provided`, HttpStatus.BAD_REQUEST)
        }

       const object = plainToClass(metatype, value);

       const errors = await validate(object);

       private isEmpty(value:any) {
           if(Object.keys(value).length < 1) {
                return true
             }
           return false
        }
    }
}

Here, we make use of the plainToClass method in the class-transformer. This converts a plain(literal) object to class(constructor) object. This is needed because we need to validate our payload as a class object. That is why we have to use the validate method from the class-validator library, it returns a Promise of Array of Errors (Promise). This will ensure that our payload is validated against the provide DTO class.

  • We need to format the errors array to make it more user friendly.
import { ArgumentMetadata, HttpException, HttpStatus, Injectable, PipeTransform } from "@nestjs/common";
import { plainToClass } from "class-transformer";
import { validate } from "class-validator";

@Injectable()
export class CustomValidationPipe implements PipeTransform {
   transform(value: any, metaData: ArgumentMetadata){
        if(this.isEmpty(value)) {
           throw new HttpException(`Validation failed: No payload provided`, HttpStatus.BAD_REQUEST)
        }

       const object = plainToClass(metatype, value);

       const errors = await validate(object);

       if (errors.length > 0) {
            throw new HttpException(`Validation failed: ${this.formatErrors(errors)}`, HttpStatus.BAD_REQUEST);
         }
      return value;

       private isEmpty(value:any) {
           if(Object.keys(value).length < 1) {
                return true
             }
           return false
       }

       private formatErrors(errors: any[]){
           return errors.map( error => {
              for (let key in error.constraints) {
                  return error.constraints[key]
               }
          }).join(', ');
        }
 }
  • We need to add the Validation pipe's class to our controller, we do that by putting it in the @ UsePipes() decorator like so;
@Post('/create')
@UsePipes(new CustomValidationPipe())
createItem(@Body() data: ItemDTO) {
    return data;
}

So there you have it. Pipes are a very useful concepts in nest.js. With the validation class that we have created, we can validate all the controllers in our application. This is very re-useable and can be easily tested in a unit test. That is our useful pipes can be in an application.

Thank you for reading along, please feel free to drop your comments and reach out to me on twitter.