Sling Academy
Home/Node.js/NestJS: How to Validate Request Body with Joi

NestJS: How to Validate Request Body with Joi

Last updated: January 01, 2024

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.

Next Article: NestJS: How to Validate Nested Objects

Previous Article: How to Inject NestJS Service from another Module

Series: Nest.js Tutorials: From Basics to Advanced

Node.js

You May Also Like

  • NestJS: How to create cursor-based pagination (2 examples)
  • Cursor-Based Pagination in SequelizeJS: Practical Examples
  • MongooseJS: Cursor-Based Pagination Examples
  • Node.js: How to get location from IP address (3 approaches)
  • SequelizeJS: How to reset auto-increment ID after deleting records
  • SequelizeJS: Grouping Results by Multiple Columns
  • NestJS: Using Faker.js to populate database (for testing)
  • NodeJS: Search and download images by keyword from Unsplash API
  • NestJS: Generate N random users using Faker.js
  • Sequelize Upsert: How to insert or update a record in one query
  • NodeJS: Declaring types when using dotenv with TypeScript
  • Using ExpressJS and Multer with TypeScript
  • NodeJS: Link to static assets (JS, CSS) in Pug templates
  • NodeJS: How to use mixins in Pug templates
  • NodeJS: Displaying images and links in Pug templates
  • ExpressJS + Pug: How to use loops to render array data
  • ExpressJS: Using MORGAN to Log HTTP Requests
  • NodeJS: Using express-fileupload to simply upload files
  • ExpressJS: How to render JSON in Pug templates