3 Commonly used input validation techniques in NestJS apps (Node.js)

Mayank Choubey
Tech Tonic
7 min readMay 20, 2024

--

Nest.js is a powerful and flexible Node.js framework for building efficient, scalable, and maintainable server-side applications. It combines elements from Object-Oriented Programming (OOP), Functional Programming (FP), and Reactive Programming (RP) to provide a robust and modular architecture. Nest.js supports TypeScript and provides a set of tools and libraries to help developers build enterprise-grade applications. Its modular design and dependency injection system make it easy to build and maintain complex applications.

Validation Techniques in Nest.js

Input validation is a very common task for any functionality built into NestJS.

Here is a quick list of the top 3 input validation techniques used by production grade Nest.js apps:

  • Class-Validator: Decorator-based validation for classes
  • Joi: Robust validation library for complex validation rules
  • Pipes: Built-in validation mechanism using reusable pipes

In this article, we’ll take a look at each technique in detail. Let’s get started with the first one.

Class-Validator

Class-Validator is a popular validation library for Nest.js that uses decorators to define validation rules for your classes. It provides a simple and intuitive way to validate data in your application, making it easier to ensure data consistency and prevent errors.

Class-Validator uses decorators to add validation metadata to the NestJS classes. We can define validation rules for properties, methods, and even entire classes using a set of built-in validation decorators. These decorators include:

  • @IsString(): Validates that a property is a string
  • @IsNumber(): Validates that a property is a number
  • @IsDate(): Validates that a property is a date
  • @IsOptional(): Validates that a property is optional
  • @IsIn(): Validates that a property is in a specific array of values
  • @Matches(): Validates that a property matches a specific regular expression

When we use class-validator, we decorate our classes with these validation decorators. Then, when we try to create an instance of the class or update an existing instance, class-validator will automatically validate the data using the defined validation rules. If the data is invalid, class-validator will throw a validation error.

Code Sample 1: Validating a user entity

Here’s an example of how we can use class-validator to validate a User entity:

import { IsString, IsEmail, IsOptional } from 'class-validator';

export class User {
@IsString()
@IsEmail()
email: string;

@IsString()
@IsOptional()
name: string;
}

In this example, we’ve decorated the email property with @IsString() and @IsEmail(), which means that the email property must be a string and a valid email address. We've also decorated the name property with @IsString() and @IsOptional(), which means that the name property must be a string, but it's optional.

Code Sample 2: Validating a create user DTO

Here’s another example of how we might use class-validator to validate a CreateUserDto:

import { IsString, IsEmail, IsOptional } from 'class-validator';

export class CreateUserDto {
@IsString()
@IsEmail()
email: string;

@IsString()
@IsOptional()
name: string;

@IsString()
password: string;
}

In this example, we’ve decorated the email property with @IsString() and @IsEmail(), which means that the email property must be a string and a valid email address. We've also decorated the name property with @IsString() and @IsOptional(), which means that the name property must be a string, but it's optional. Finally, we've decorated the password property with @IsString(), which means that the password property must be a string.

Class-validator is a powerful and easy-to-use validation library for Nest.js that uses decorators to define validation rules for our classes. By using class-validator, we can ensure that our data is consistent and valid, and prevent errors in our application.

Joi

Joi is a popular validation library for Nest.js that provides a robust and flexible way to validate data in our application. Unlike class-validator, which uses decorators to define validation rules, Joi uses a schema-based approach to define validation rules.

With Joi, we define a validation schema using a simple and intuitive API. The schema defines the structure and constraints of the data we want to validate. When we validate data against the schema, Joi checks the data against the defined constraints and returns an error if the data is invalid.

Joi provides a wide range of validation rules, including:

  • String validation (e.g., email, phone number, etc.)
  • Number validation (e.g., minimum, maximum, etc.)
  • Array validation (e.g., length, contents, etc.)
  • Object validation (e.g., properties, nested objects, etc.)

Code Sample 1: Validating a user registration form

Here’s an example of using Joi to validate a user registration form:

const Joi = require('joi');

const userRegistrationSchema = Joi.object().keys({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
name: Joi.string().required()
});

const userData = {
email: 'invalid-email',
password: 'short',
name: 'John Doe'
};

const result = Joi.validate(userData, userRegistrationSchema);

if (result.error) {
console.error(result.error);
} else {
console.log('Data is valid!');
}

In this example, we define a Joi schema for a user registration form using the Joi.object().keys() method. We define three properties: email, password, and name. Each property has specific validation rules: email must be a valid email address, password must be at least 8 characters long, and name is required. We then validate the userData object against the schema using the Joi.validate() method. If the data is invalid, we log the error; otherwise, we log a success message.

Code Sample 2: Validating a payment request

Here’s an example of how we can use Joi to validate a payment request:

const Joi = require('joi');

const paymentRequestSchema = Joi.object().keys({
amount: Joi.number().min(1).required(),
paymentMethod: Joi.string().valid('creditCard', 'paypal').required(),
billingAddress: Joi.object().keys({
street: Joi.string().required(),
city: Joi.string().required(),
state: Joi.string().required(),
zip: Joi.string().required()
}).required()
});

const paymentData = {
amount: 0,
paymentMethod: 'invalid',
billingAddress: {
street: '123 Main St',
city: 'Anytown',
state: 'CA',
zip: '12345'
}
};

const result = Joi.validate(paymentData, paymentRequestSchema);

if (result.error) {
console.error(result.error);
} else {
console.log('Data is valid!');
}

In this example, we define a Joi schema for a payment request using the Joi.object().keys() method. We define three properties: amount, paymentMethod, and billingAddress. Each property has specific validation rules: amount must be a number greater than or equal to 1, paymentMethod must be either 'creditCard' or 'paypal', and billingAddress must be an object with specific properties (street, city, state, and zip). We then validate the paymentData object against the schema using the Joi.validate() method. If the data is invalid, we log the error; otherwise, we log a success message.

Joi is a robust and flexible validation library for Nest.js that provides a schema-based approach to defining validation rules. With its wide range of validation rules and intuitive API, Joi is a great choice for any Nest.js application that requires complex data validation. The only catch is that, it is not decorator based. So, might feel a bit antiquated.

Pipes

Pipes are a built-in validation mechanism in Nest.js that allows you to transform and validate data in a flexible and reusable way. Unlike Class-Validator and Joi, which focus on defining validation rules for specific classes or schemas, Pipes provide a more general-purpose solution for data validation and transformation.

How Pipes Work

In Nest.js, Pipes are classes that implement the PipeTransform interface. This interface has two methods: transform() and metis(). The transform() method is responsible for transforming and validating the data, while the metis() method is responsible for returning a metadata object that describes the transformation.

Pipes can be used in various ways in Nest.js, including:

  • Validation: Pipes can be used to validate data using custom validation logic.
  • Transformation: Pipes can be used to transform data from one format to another.
  • Sanitization: Pipes can be used to sanitize data, such as trimming strings or removing unnecessary characters.

You will wonder why this is the last in the list. The reason is that, it is cumbersome to use.

To use pipes in Nest.js, we can inject them into our controllers and use them to validate and transform data. For example:

import { Controller, Post, Body } from '@nestjs/common';
import { UsernamePipe } from './username.pipe';
import { DatePipe } from './date.pipe';

@Controller('users')
export class UsersController {
@Post('create')
createUser(@Body(UsernamePipe) username: string, @Body(DatePipe) dateOfBirth: Date) {
// username and dateOfBirth are now validated and transformed
}
}

In this example, we define a UsersController class with a createUser() method that takes two parameters: username and dateOfBirth. We use the @Body() decorator to inject the UsernamePipe and DatePipe instances, which validate and transform the input data accordingly.

Code Sample 1: Validating a username with a custom pipe

Here’s an example of using a pipe to validate a username:

import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common';

export class UsernamePipe implements PipeTransform<string> {
transform(username: string, metadata: ArgumentMetadata) {
if (!username.match(/^[a-zA-Z0-9_]+$/)) {
throw new BadRequestException('Invalid username');
}
return username;
}
}

In this example, we define a UsernamePipe class that implements the PipeTransform interface. The transform() method checks if the username matches a regular expression pattern. If it doesn't, it throws a BadRequestException. Otherwise, it returns the validated username.

Code Sample 2: Transforming a date string with a custom pipe

Here’s another example of how we can use a pipe to transform a date string:

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

export class DatePipe implements PipeTransform<string> {
transform(dateString: string, metadata: ArgumentMetadata) {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
throw new BadRequestException('Invalid date format');
}
return date;
}
}

In this example, we define a DatePipe class that implements the PipeTransform interface. The transform() method takes a date string as input, creates a new Date object from it, and checks if the date is valid. If it's not, it throws a BadRequestException. Otherwise, it returns the transformed date object.

Pipes are a flexible and reusable validation technique in Nest.js that allow us to transform and validate data in a variety of ways. By defining custom Pipes, we can encapsulate complex validation logic and reuse it throughout your application.

Still, there is too much work to use the built-in mechanism. My personal choice is class-validator.

Thanks for reading!

--

--