(NestJS-7)Overview of Validations and Pipes in NestJS
In the modern web development landscape, ensuring data accuracy and integrity is critical. In a NestJS application, validations and pipes play a pivotal role in safeguarding your APIs by managing and transforming incoming data. This article explores these powerful tools, focusing on Data Transfer Object (DTO) validations and the versatility of pipes within NestJS controllers. By the end of this guide, you’ll have a solid understanding of how to implement and leverage validations and pipes to create robust and secure APIs.
Prerequisites
Before we start, make sure you have the following:
- NestJS Project Setup: A basic NestJS project. If you don’t have one, you can refer here.
- TypeORM and MySQL: Installed and configured in your project, refer here.
- Entities Relation: Defining relationships between entities, refer here.
- REST APIs: Defining API’s with data bodies, refer here.
Introduction to Pipes in NestJS
In NestJS, pipes are a core concept that allows you to perform transformations and validations on the data before it reaches to controller. Pipes can be applied at different levels:
- Global: Applied to all routes in the application.
- Controller: Applied to all routes within a controller.
- Route Handler: Applied to a specific route handler.
Why Do We Need Validations?
Let’s take a simple example: someone sends a request to create a user but leaves the email field blank. Without validation, that request could slip through and cause chaos in your app. Validations act as your first line of defense, making sure all incoming data fits your criteria before it goes any further.
Types of Pipes
1. Built-In Pipes
NestJS gives us several pipes right out of the box. Let’s look at the two most common ones:
i. ValidationPipe: This is the go-to pipe for validating incoming requests based on the rules you define in your DTOs. We’ll see how it works with an example later.
ii. TransformationPipe: This pipe transforms data into the desired format. For instance, if you’re expecting an integer but receive a string, the pipe will convert it for you.
2. Custom pipes
You can also create custom pipes to handle specific data validation or transformation. Custom pipes can be used with controllers, DTOs, and more.
If validation fails, the pipe will throw an exception, which can be handled by the exceptions layer (global exceptions filter). we will try to implenent this layer in upcoming articles.
Installations
To begin using it, we first install the required dependency.
npm i --save class-validator class-transformer
Adding Class Validation in DTOs
DTOs are a powerful tool in NestJS that define the structure of the data being sent and received by your API. By integrating validation decorators provided by class-validator
, you can easily enforce rules for each property in your DTO.
import { IsString, IsEmail, IsNotEmpty, Length, IsOptional, IsDateString } from 'class-validator';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
name: string;
@IsEmail()
@IsNotEmpty()
email: string;
@IsString()
@Length(8, 20)
@IsNotEmpty()
password: string;
@IsString()
@Length(8, 10)
@IsNotEmpty()
phoneno: string
@IsString()
@IsNotEmpty()
address: string;
@IsDateString()
@IsOptional()
dob?: Date;
@IsString()
@IsOptional()
bio?: string;
}
In this example, the CreateUserDto
class uses several validation decorators:
@IsString()
: Ensures that the value is a string.@IsEmail()
: Validates that the value is a properly formatted email.@IsNotEmpty()
: Ensures that the field is not empty.@Length(MIN, MAX)
: Restricts the length of the string to betweenMIN
andMAX
characters.@IsOptional()
: Marks the field as optional, meaning it won’t be required.@IsDateString()
: Validates that the value is a valid ISO 8601 date string.
Example of a CreateOrderDto with Class-Validation
import { IsString, IsUUID, IsArray, ArrayNotEmpty, IsNotEmpty } from 'class-validator';
export class CreateOrderDto {
@IsString()
@IsUUID() // Ensures the userId is a valid UUID
@IsNotEmpty() // Ensures the userId is not empty
userId: string;
@IsArray() // Ensures productIds is an array
@ArrayNotEmpty() // Ensures the array is not empty
@IsUUID("4", { each: true }) // Ensures each element in the array is a valid UUID
productIds: string[];
}
@IsUUID()
: Ensures the value is a valid UUID (Version 4 by default, for more refer here.).@IsNotEmpty()
: Ensures the field is not empty.@IsArray()
: Validates that the field is an array.@ArrayNotEmpty()
: Ensures the array is not empty.@IsUUID("4", { each: true })
: This applies the UUID validation to each element of the array.
Here you can find more class validators which are present in library.
Note:- Till now we tried to see DTO validation. In below step transformation pipe is used for transform & validate params.
Setting a Validation/Transformation Pipe for params
As we discussed about the transformation pipe, it will transform data from one format into another format (e.g., string to integer). The below is the example useage of transformation pipe.
Note: The pipe may throw if the conversion fails.
import { Get, Param, ParseIntPipe } from '@nestjs/common';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: string) {
return this.userService.findOne(id);
}
}
We're not using ParseIntPipe in our development code, because our primary ID is defined as a string (UUID).
Applying Validation Pipe
NestJS provides the ValidationPipe
to enforce the rules specified in your DTOs. It can be applied globally or at the controller/route handler level.
Applying Validation Globally
This apply at the application level, thus ensuring all endpoints are protected from receiving incorrect data.
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe()); // Applies validation globally
await app.listen(3000);
}
bootstrap();
Applying Validation at the Controller Level
This apply at the controlller level, only the specified controller is protected from receiving incorrect data.
import { Controller, Post, Body, UsePipes } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UserService } from './user.service';
import { ValidationPipe } from '@nestjs/common';
@Controller('user ')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
@UsePipes(new ValidationPipe())
async createUser(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
}
In this example, the ValidationPipe
is applied to the createOrder
route to ensure that all incoming requests are validated according to the rules defined in respective DTO
.
Working with Custom Pipes
Sometimes, you may need a custom pipe to handle specific data validation or transformation. For instance, you can create a pipe to validate UUIDs.
// src/config/custom/parse-uuid.pipe.ts
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseUUIDPipe implements PipeTransform<string, string> {
transform(value: string): string {
if (!this.isUUID(value)) {
throw new BadRequestException('Invalid UUID');
}
return value;
}
private isUUID(value: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(value);
}
}
Every pipe must implement the transform() method to fulfill the PipeTransform interface contract. This method has two parameters:
- Value: the argument that we are passing on to the Pipe
- Metadata: meta details of the arguments
This pipe checks if the incoming value is a valid UUID, if not throws an exception. You can then apply this custom pipe in your controller like this:
import { Controller, Get, Param } from '@nestjs/common';
import { ParseUUIDPipe } from 'src/config/custom/parse-uuid.pipe';
import { UserService } from './user.service';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.userService.findOne(id);
}
}
Sample Error Messages
Here’s what happens when validations fail:
// password and address variable missing in DTO in POST request
{
"message": [
"password must be longer than or equal to 8 characters",
"password must be a string",
"address must be a string"
],
"error": "Bad Request",
"statusCode": 400
}
// Built-in UUID validation error
{
"message": [
"userId must be a UUID",
"each value in productIds must be a UUID"
],
"error": "Bad Request",
"statusCode": 400
}
Note:- By using above examples as reference please add the class validators' to all DTOs’
References
Conclusion
In this article, we delved into the essentials of validations and pipes in a NestJS application. By using DTOs with class-validator
and leveraging the ValidationPipe
, developers can build APIs that are not only robust but also secure and user-friendly. Pipes extend this functionality by enabling seamless data transformations, enhancing the overall development experience.
Stay tuned for the next article in the series, where we will explore how to encrypt password before storing it in a database.
GitHub Repository:
You can find the complete code for this implementation on GitHub: bhargavachary123
Contribution
If you like my Article, please, consider donating through Buy Me a Coffee: https://buymeacoffee.com/bhargavachary
If you found this article useful, please consider leaving a clap (👏) and a comment.
Stay tuned for more articles covering advanced topics in NestJS development! Happy coding ⚔️