How to Implement Data Validation in NestJS Using nestjs-joi and joi-class-decorators

alifathy1999

AliFathy-1999

Posted on June 20, 2024

How to Implement Data Validation in NestJS Using nestjs-joi and joi-class-decorators

Here are some topics I can cover:

  1. Introduction to Data Validation in Web Applications.
  2. Setting Up NestJS for Validation.
    • Prerequisites and project setup.
    • Creating and Using Validation Schemas
  3. Introduction to pipe and how to use it with Joi
  4. Validation on Param and Query Params using Joi.
  5. Practical Implementation: Explore a complete NestJS application utilizing nestjs-joi and joi-class-decorators on GitHub: NestJS Sample App with nestjs-joi.

1. Introduction to Data Validation in Web Applications.

  • Data validation is the process of ensuring that data entered by users or obtained from external sources satisfies the specified criteria and format. Data validation can be done at several levels, including client-side, server-side, and database-level.

2. Setting Up NestJS for Validation.

  • Prerequisites and project setup:

1. Install Node and npm :

Make sure that you installed Node on your device

node -v to detect whether you have installed the Node or not. If you installed node output is v20.13.1 or any version. If you didn't install node output will be node: command not found.

You need to install node by going to Nodejs website NodeJS website

Make sure that you installed Node Package Manager npm on your device npm -v to detect whether you have installed the npm or not. If you installed npm output is 10.8.0 or any version. If you didn't install npm output will be npm: command not found.

2. Install NestJs and create new nestApp :

npm i -g @nestjs/cli
nest new my-nestjs-app
cd ./my-nestjs-app
Enter fullscreen mode Exit fullscreen mode

3. create a new pipe called validation:

// --no-spec => Disable spec files generation 
// --flat => Do not generate a folder for the element.
nest g pipe validation --no-spec --flat
Enter fullscreen mode Exit fullscreen mode

4. Installing necessary packages (nestjs-joi, joi-class-decorators)

npm i class-transformer joi nestjs-joi joi-class-decorators
Enter fullscreen mode Exit fullscreen mode
  • Creating and Using Validation Schemas:

1. Create Endpoint '/testBody', Method type: Post,In app controller

import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common';
import { AppService } from './app.service';
import { Request, Response } from 'express';
import { validationBodyDto } from './validate.dto';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Post('/testBody')
  @HttpCode(HttpStatus.OK)
  testJoiValidation(@Req() req: Request, @Res() res: Response, @Body() reqBody: validationBodyDto) {
    const data = reqBody;
    res.json(data);
  }
}

Enter fullscreen mode Exit fullscreen mode

2. Create Dto file called (validate.dto.ts) to validate this endpoint and create joi schema class (validationBodyDto):

import { Expose } from "class-transformer";
import { JoiSchema, JoiSchemaOptions } from "joi-class-decorators";
import * as Joi from 'joi';

interface reviewInterface {
    rating: number;
    comment: string;
}
// @Expose ==> is used to mark properties that should be included in the transformation process, typically for serialization and deserialization. However.
// @JoiSchema() ==> Define a schema on a type (class) property. Properties with a schema annotation are used to construct a full object schema.


//It ensures strict validation by disallowing any properties that are not explicitly defined in your schema. 
@JoiSchemaOptions({
    allowUnknown: false
})

export class validationBodyDto {

    //Basic Validation is type string and required
    @Expose() @JoiSchema(Joi.string().trim().required())
    fullName: string;

    //Check on length, and is valid egyptian phone number
    @Expose() @JoiSchema(Joi.string().length(11).pattern(/^(011|012|015|010)\d{8}$/).required())
    phoneNumber: string;

    //Check is valid email
    @Expose() @JoiSchema(Joi.string().email().optional())
    email?: string;

    //Check value is valid in case of M or F only
    @Expose() @JoiSchema(Joi.string().valid('M', 'F').required())
    gender: string;

    //militaryStatus is mendatory if gender is M otherwise is optional
    @Expose() @JoiSchema(
        Joi.when('gender', {
            is: 'M',
            then: Joi.string().required(),
            otherwise: Joi.string().optional(),
        }),
    )
    militaryStatus: string;

    //Check age is number, min 14 and max age is 100
    @Expose() @JoiSchema(Joi.number().min(14).max(100).message('Invalid age'))
    age: number;

    //Check on Array of object is valid or invalid
    @Expose()
    @JoiSchema(
        Joi.array().items(
                Joi.object({
                        rating: Joi.number().min(0.1).required(),
                        comment: Joi.string().min(3).max(300).required(),
                    }).required(),
            ).required(),
    )
    reviews: reviewInterface[];

    //allow this field with empty string
    @Expose() @JoiSchema(Joi.string().allow('').optional())
    profilePicture?: string;

    //profileFileName is mendatory if profilePicture has an value otherwise it optional 
    @Expose() @JoiSchema(
        Joi.when('profilePicture', {
            is: Joi.string().exist(),
            then:  Joi.string().required(),
            otherwise: Joi.string().allow('').optional(),
    }))
    profileFileName: string;

    //Check if isVerified is boolean and required
    @Expose() @JoiSchema(Joi.boolean().required())
    isVerified: boolean;

}
Enter fullscreen mode Exit fullscreen mode

3. Introduction to pipe and how to use it with Joi

  • In NestJS, a "pipe" is a class annotated with the @Injectable() decorator that implements the PipeTransform interface. Pipes are typically used for transforming or validating data. They can be used at various levels, including method-level, controller-level, or globally.
  • Introduction to Pipes
    • Transformation: Pipes can transform input data to a desired format.
    • Validation: Pipes can validate the data before passing it to the request handler. If the data is invalid, the pipe can throw an exception, which will be handled by NestJS.
  • In our case, we use it to transform plain object into a typed object so that we can apply validation.
  • So let us use the validation pipe that we created before:
import { BadRequestException, Injectable, PipeTransform, Type } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { getClassSchema } from 'joi-class-decorators';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    const { metatype } = metadata;
    const bodyDto = metatype; // Dto Schema
    /*
      To transform our plain JavaScript argument object into a typed object so that we can apply validation.
      The reason we must do this is that the incoming post body object, when deserialized from the network request, does not have any type information. 
    */

    // getClassSchema(bodyDto) ==> A function from joi-class-decorators to retrieve the Joi validation schema associated with a class.
    const bodyInput = plainToInstance(bodyDto, value); // Convert plain Dto object to instance to transform it manually
    const bodySchema = getClassSchema(bodyDto); // Get Joi Schema from Dto
    // Validates the class instance against the Joi schema. If validation fails, error will contain the validation errors.
    const error = bodySchema.validate(bodyInput).error;  
    if (error) {
      throw new BadRequestException(`Validation failed: ${error.details.map((err) => err.message).join(', ')}.`);  
    }
    return value
  }
}
interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}
Enter fullscreen mode Exit fullscreen mode
  • To use this validation pipe on our endpoint, we have four ways:
    • Use Global scoped pipes, It will be applied on every route handler across the entire application.
// In main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode
  • Use parameter-scoped pipes, It will be applied on param reqBody.
  @Post('/testBody')
  @HttpCode(HttpStatus.OK)
  testJoiValidation(@Body(new ValidationPipe()) reqBody: validationBodyDto, @Res() res: Response) {
    const data = reqBody;
    res.json(data);
  }
Enter fullscreen mode Exit fullscreen mode
  • Use method-scoped pipes, It will be applied on method testJoiValidation.
  @Post('/testBody')
  @HttpCode(HttpStatus.OK)
  @UsePipes(new ValidationPipe()) // Method Scope
  testJoiValidation(@Body() reqBody: validationBodyDto, @Res() res: Response) {
    const data = reqBody;
    res.json(data);
  }
Enter fullscreen mode Exit fullscreen mode
  • Use controller-scoped pipes, It will be applied on method testJoiValidation.
@Controller()
@UsePipes(new ValidationPipe()) //Controller-scoped
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Post('/testBody')
  @HttpCode(HttpStatus.OK)
  testJoiValidation(@Body() reqBody: validationBodyDto, @Res() res: Response) {
    const data = reqBody;
    res.json(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Validation on Param and Query Params using Joi.

  • Create an endpoint '/testParams/:category', method type: GET, It took param named category ('Fashions', 'Electronics', 'MobilesPhones', 'Perfumes') and two Query Params limit and page.
  @Get('/testParams/:category')
  @HttpCode(HttpStatus.OK)
  @UsePipes(new ValidationPipe())
  testJoiValidationParam(
    @Param() category: validationParamDto,
    @Query() limitAndPageSize: validationQueryParamDto,
    @Res() res: Response
  ) {
    res.json({
      category,
      limitAndPageSize
    });
  }
Enter fullscreen mode Exit fullscreen mode
  • Create two dtos for those params:
export class validationParamDto {
    @Expose() @JoiSchema(Joi.string().valid('Fashions', 'Electronics', 'MobilesPhones', 'Perfumes').required())
    category: string;
}

@JoiSchemaOptions({
    allowUnknown: false
})

export class validationQueryParamDto {

    @Expose() @JoiSchema(Joi.number().min(0).max(100).message('Invalid limit'))
    limit: string;

    @Expose() @JoiSchema(Joi.number().min(0).max(100).message('Invalid page size'))
    page: string;
}
Enter fullscreen mode Exit fullscreen mode

Finally, I want to thank you for taking the time to read my article and I hope this article is useful for you :).

For hands-on implementation and further exploration, you can access the complete codebase of a sample NestJS application using nestjs-joi and joi-class-decorators on GitHub. The repository includes practical examples and configurations demonstrating how to integrate and leverage robust data validation in NestJS:

Explore the NestJS Sample App on GitHub

Feel free to clone, fork, or contribute to enhance your understanding and implementation of data validation in NestJS.

💖 💪 🙅 🚩
alifathy1999
AliFathy-1999

Posted on June 20, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related