NestJS: How to Validate Request Body with Joi

Updated: January 1, 2024 By: Guest Contributor Post a comment

Introduction

Validating incoming request data is crucial for the security and integrity of any web application. In this tutorial, we will explore how to use Joi with NestJS to ensure request body data adheres to predefined schemas.

Setting Up Your NestJS Project

To begin, make sure you have Node.js installed. Then, install the NestJS CLI globally:

npm i -g @nestjs/cli

Create a new NestJS project using the CLI:

nestjs new your-project-name

Once generated, navigate into your new project directory and open the project in your favorite editor.

Installing Joi

Joi is a powerful schema description language and data validator for JavaScript. To include Joi in your NestJS project, run:

npm install joi

Basic Validation

First, let’s define a basic schema for a user object. Create a file named user.schema.ts and define your Joi schema:

import * as Joi from 'joi';

export const UserSchema = Joi.object({
    username: Joi.string().alphanum().min(3).max(30).required(),
    password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}

This schema ensures that the username is alphanumeric and between 3 to 30 characters, the password matches a specific regular expression, and the email is a valid email address.

Integrating Joi with NestJS

Now, let’s integrate Joi into a NestJS controller. Start by creating a user.controller.ts file:

import { Controller, Post, Body } from '@nestjs/common';
import { UserSchema } from './user.schema';
import * as Joi from 'joi';

@Controller('user')
export class UserController {
    @Post()
    createUser(@Body() body: any) {
        const result = UserSchema.validate(body);
        if (result.error) {
            throw new BadRequestException(result.error.details);
        }
        // Continue with your user creation logic...
    }
}

In this example, you can see how we use the UserSchema to validate the request body. If it fails validation, a bad request exception is thrown.

Validation with Custom Decorators

While the method above works, NestJS offers a more elegant solution using custom decorators and pipes. Define a validation pipe:

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

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

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

Next, use this pipe in your controller:

import { Controller, Post, Body, UsePipes } from '@nestjs/common';
import { UserSchema } from './user.schema';
import { JoiValidationPipe } from './joi-validation.pipe';

@Controller('user')
export class UserController {
    @Post()
    @UsePipes(new JoiValidationPipe(UserSchema))
    createUser(@Body() body: any) {
        // Your user creation logic...
    }
}

With custom decorators and pipes, you gain reusability and can make validation more readable and maintainable.

Advanced Validation Techniques

Leveraging groups and custom messages with Joi allows for finer control:

export const UserSchema = Joi.object({
    username: Joi.string().alphanum().min(3).max(30).required().messages({
        'string.base': 'Username must be a string',
        'string.alphanum': 'Username must be alphanumeric',
        // More custom messages...
    }),
    // Other validations...
});

Moreover, using async validation in NestJS with Joi:

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
import * as Joi from 'joi';

@Injectable()
export class JoiValidationPipe implements PipeTransform {
    // ...

    async transform(value: any, metadata: ArgumentMetadata) {
        try {
            await this.schema.validateAsync(value);
        } catch (error) {
            throw new BadRequestException('Validation failed');
        }
        return value;
    }
}

Async validation can be beneficial when incorporating asynchronous custom validation functions within your Joi schema.

Error Handling and Custom Responses

Besides the basic error handling shown earlier, you may want to format your error responses. You can do this by adjusting the error thrown in your validation pipe:

import { BadRequestException } from '@nestjs/common';

// Inside the validation pipe:
if (error) {
    throw new BadRequestException({
        statusCode: 400,
        message: error.details.map(detail => detail.message),
        error: 'Bad Request',
    });
}

This approach provides clients with more informative and consistent error messages.

Validation as a Global Middleware

For project-wide consistency, you can set up validation globally. In your main application file, usually main.ts add:

// ...
import { JoiValidationPipe } from './path-to-pipe/joi-validation-pipe';
import { UserSchema } from './path-to-schema/user.schema';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new JoiValidationPipe(UserSchema));
  // The rest of your bootstrap function.
}
bootstrap();

By registering the pipe globally, all your routes will be covered, providing a consistent and secure validation layer across your entire application.

Conclusion

In this tutorial, we’ve covered the basics of using Joi for request body validation in a NestJS application, explored custom decorators and pipes, discussed advanced techniques, and looked at global implementations. With Joi’s extensive possibilities and NestJS’s clean, modular architecture, you’re well-equipped to create robust and secure web applications.