NestJS comes with in-built pipes. However, many times it is still important to configure your own custom NestJS Pipe. We are going to cover the same in this post.

Before we start, I would recommend you to go through our detailed post about NestJS Pipes and NestJS ValidationPipe. If you are already aware about these topics, then you can continue right on.

1 – Creating a custom NestJS Pipe

Let’s start by trying to build our own custom NestJS ValidationPipe. As a first step, we will keep things simple. Our custom pipe will simply return the input value.

Below is the example of such a pipe.

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

@Injectable()
export class CustomValidationPipe implements PipeTransform {
    transform(value: any, metadata: ArgumentMetadata) {
        return value;
    }
}

Let us understand what is going on in this code snippet.

  • Every custom pipe must implement the PipeTransform interface. PipeTransform<T, R> is a generic interface. Here, T is the input value and R is the output.
  • As part of the interface contract, our custom pipe implements the transform() method.
  • Basically, the transform() method has two inputs – value and metadata.
  • The value parameter is the method’s input parameter. In other words, the parameter before it reaches the request handler.
  • Metadata, on the other hand, is the currently process argument’s metadata.

2 – The Argument Metadata

The Argument Metadata is another in-built interface that has the below properties. Below is the code for this interface.

export interface ArgumentMetadata {
    readonly type: Paramtype;
    readonly metatype?: Type<any> | undefined;
    readonly data?: string | undefined;
}

These properties describe the currently processed argument.

  • TYPE – Indicates whether the argument is @Body, @Query, @Param or some other custom type.
  • METATYPE – Provides the meta-type or the argument. For example, String type.
  • DATA – This is the string passed to the decorator. For example, @Body(‘string’). It’s value is undefined if we leave the decorator parantheses empty.

3 – Custom ValidationPipe

Let us now enhance our basic custom pipe to handle some sort of validation.

Assume that we have the below controller.

import { Body, Post } from "@nestjs/common";
import { CreateEmpDTO } from "./create-emp-dto";

export class EmployeeController {
    @Post()
    async create(@Body() createEmpDTO: CreateEmpDTO) {
        console.log("Calling the Employee Create Service")
    }
}

Basically, this controller takes a CreateEmpDTO as input and tries to create a new employee. In the example, we just have the console log statement. However, you can imagine that we would be calling a service. Ideally, we would want the input payload to be validated before we actually call the create employee service.

Below is the CreateEmpDTO class.

export class CreateEmpDTO {
    name: string;
    designation: string;
}

Basically, we want to make sure that any incoming request to our create employee request handler should have a valid body.

There are a couple of approaches to handle the validations.

3.1 – Object Schema Validation

The first approach is to use schema-based validation. We will use the Joi library to achieve the same. Joi is a standard library for describing schemas and validating data.

To make use of Joi, we will first install the below packages to our project.

$ npm install --save joi
$ npm install --save-dev @types/joi

Now, we can enhance our custom pipe implementation as below:

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

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

    transform(value: any, metadata: ArgumentMetadata) {
        const { error } = this.schema.validate(value);

        if(error) {
            throw new BadRequestException('Invalid Input Data');
        }

        return value;
    }
}

Basically, the constructor of our custom pipe class takes an instance of ObjectSchema as input. We then call the validate() method. This method validates the incoming argument against the schema. In case of mismatch, we throw a BadRequestException.

Now, we can bind our custom pipe to the create employee request handler. This can be done using the @UsePipes decorator.


export class EmployeeController {
    @Post()
    @UsePipes(new CustomValidationPipe(createEmpSchema))
    async create(@Body() createEmpDTO: CreateEmpDTO) {
        console.log("Calling the Employee Create Service")
    }
}

3.2 – Class Validator Approach

An alternate approach to handle validations is to use the class-validator library. As we saw in the NestJS ValidationPipe post, class-validator goes well with NestJS. Basically, it allows us to use powerful decorators to make declarative validations.

We can install class-validator using the below command.

npm i --save class-validator class-transformer

Next, we will add few decorators to our CreateEmpDTO class as below:

import { IsString } from "class-validator";

export class CreateEmpDTO {
    @IsString()
    name: string;

    @IsString()
    designation: string;
}

The main advantage here is that the DTO is the single source of truth. We don’t need an additional schema class for validation.

Now, we need to modify the custom validation pipe. See below code:

import { ArgumentMetadata, BadRequestException } from "@nestjs/common";
import { Injectable, PipeTransform } from "@nestjs/common";
import { plainToClass } from "class-transformer";
import { validate } from "class-validator";

@Injectable()
export class CustomValidationPipe implements PipeTransform<any> {

    async transform(value: any, {metatype}: ArgumentMetadata) {
        if (!metatype || !this.validateMetaType(metatype)){
            return value;
        }

        const object = plainToClass(metatype, value);
        const errors = await validate(object);

        if(errors.length > 0) {
            throw new BadRequestException('Invalid Input Data')
        }

        return value;

    }

    private validateMetaType(metatype: Function): boolean {
        const types: Function[] = [String, Boolean, Number, Array, Object];
        return !types.includes(metatype)
    }
}

Few important points about the above code is as follows:

  • We make the transform() method as async. This is because certain other methods as part of class-validator can be asynchronous in nature.
  • Next, we extract the metatype attribute from ArgumentMetaData using object de-structuring. We use the metatype attribute to bypass the validation in case we are dealing with native javascript object that do not have a type. Basically, we use a helper method validateMetaType for this purpose.
  • Next, we use the plainToClass() function to transform our input request object into a Type object so that we can perform validation on the same.
  • Finally, we call the validate() function to perform the validation. In case of errors, we throw BadRequestException. Else, we return the value.

At the end, we can simply bind our custom pipe with the request handler.

import { Body, Post, UsePipes } from "@nestjs/common";
import { CreateEmpDTO } from "./create-emp-dto";
import { CustomValidationPipe } from "./custom-pipe";

export class EmployeeController {
    @Post()
    @UsePipes(new CustomValidationPipe())
    async create(@Body() createEmpDTO: CreateEmpDTO) {
        console.log("Calling the Employee Create Service")
    }
}

Conclusion

With this, we are done with configuring a custom NestJS Pipe for our application. We basically looked at multiple approaches we can achieve validations in our custom pipe and also understood the various steps needed.

If you have any comments or queries about the topic, please feel free to write in the comments section below.


Saurabh Dashora

Saurabh is a Software Architect with over 12 years of experience. He has worked on large-scale distributed systems across various domains and organizations. He is also a passionate Technical Writer and loves sharing knowledge in the community.

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *