In modern web development, validating incoming data is crucial for ensuring that your application behaves as expected and maintains data integrity. DTOs (Data Transfer Objects) are used to define the structure of incoming data, and validation ensures that the data is in the right format. In this article, we’ll dive into how to work with DTOs and validation using the class-validator
library in NestJS.
Table of Contents
- Introduction
- What are DTOs (Data Transfer Objects)?
- Setting Up Validation with
class-validator
- Creating a DTO Class
- Common
class-validator
Decorators - Using DTOs and Validation in Controllers
- Custom Validation Messages
- Global vs Local Validation
- Transforming Data with
class-transformer
- Creating Custom Validation Decorators
- Error Handling in Validation
- Best Practices for Validation
Introduction
Data validation is a crucial aspect of building secure and reliable applications. In NestJS, validation of incoming data is achieved using DTOs (Data Transfer Objects) and the class-validator
package. DTOs define the shape of data coming into your application, while validation ensures that the data adheres to expected formats, preventing issues like incorrect data types or malformed input.
In this article, we’ll explore how to create DTOs, apply validation rules, and handle validation errors using class-validator
in a NestJS application.
What are DTOs (Data Transfer Objects)?
A DTO (Data Transfer Object) is a design pattern used to define the structure of data transferred between different parts of an application. DTOs are typically used to define the shape of incoming requests (like POST or PUT) or outgoing responses.
In NestJS, DTOs are usually implemented as classes that contain properties representing the data fields. They can also include validation rules to ensure the data is valid.
For example, you might have a CreateUserDto
for creating new users, with properties like firstName
, lastName
, email
, and age
.
Setting Up Validation with class-validator
Before you can use class-validator
, you need to install it in your NestJS project.
Install Dependencies
To install the necessary dependencies, run the following command:
bashCopyEditnpm install class-validator class-transformer
class-validator
provides decorators to define validation rules.class-transformer
helps in transforming the plain JavaScript objects into class instances.
Enable Global Validation
To ensure that all incoming requests are validated automatically, you need to enable the ValidationPipe globally in the application. This can be done in your main.ts
file.
typescriptCopyEditimport { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
The ValidationPipe will automatically validate all incoming data based on the DTOs used in the request bodies.
Creating a DTO Class
Here’s an example of a simple DTO that validates the data for creating a user:
typescriptCopyEditimport { IsString, IsInt, IsEmail, Min, Max, IsOptional } from 'class-validator';
export class CreateUserDto {
@IsString()
@IsOptional()
firstName: string;
@IsString()
lastName: string;
@IsEmail()
email: string;
@IsInt()
@Min(18)
@Max(100)
age: number;
}
In this example:
@IsString()
ensures thatfirstName
andlastName
are strings.@IsEmail()
validates thatemail
is a valid email address.@IsInt()
,@Min()
, and@Max()
are used to ensure thatage
is an integer between 18 and 100.@IsOptional()
makesfirstName
optional.
Common class-validator
Decorators
Here are some commonly used decorators provided by class-validator
:
@IsString()
: Ensures the field is a string.@IsInt()
: Ensures the field is an integer.@Min(value: number)
: Ensures the field is greater than or equal to the specified value.@Max(value: number)
: Ensures the field is less than or equal to the specified value.@IsEmail()
: Validates that the field is a valid email address.@IsOptional()
: Marks a field as optional.@IsNotEmpty()
: Ensures the field is not empty.
Using DTOs and Validation in Controllers
Once you’ve defined your DTOs, you can use them in your controllers to validate incoming requests. Here’s how to use the CreateUserDto
in a controller:
typescriptCopyEditimport { Body, Controller, Post } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UserController {
@Post()
create(@Body() createUserDto: CreateUserDto) {
return `User ${createUserDto.firstName} created successfully!`;
}
}
In this example, the createUserDto
is automatically validated against the CreateUserDto
class, and any validation errors will result in a 400 Bad Request
response.
Custom Validation Messages
You can provide custom validation error messages using the message
option. This helps make the error messages more user-friendly.
typescriptCopyEditexport class CreateUserDto {
@IsString({ message: 'First name must be a string' })
@IsOptional()
firstName: string;
@IsString({ message: 'Last name must be a string' })
lastName: string;
@IsEmail({}, { message: 'Email must be a valid email address' })
email: string;
@IsInt({ message: 'Age must be an integer' })
@Min(18, { message: 'Age must be at least 18' })
@Max(100, { message: 'Age cannot exceed 100' })
age: number;
}
Global vs Local Validation
You can apply validation globally to all routes, or locally to specific routes. Here’s how to apply the ValidationPipe globally, which we already discussed:
Global Validation (App-wide)
typescriptCopyEditapp.useGlobalPipes(new ValidationPipe());
Local Validation (Per Route)
Alternatively, you can apply the ValidationPipe
to individual routes using the @UsePipes()
decorator:
typescriptCopyEditimport { UsePipes, Body, Post, Controller } from '@nestjs/common';
import { ValidationPipe } from '@nestjs/common';
@Controller('users')
export class UserController {
@Post()
@UsePipes(new ValidationPipe())
create(@Body() createUserDto: CreateUserDto) {
return `User ${createUserDto.firstName} created successfully!`;
}
}
Transforming Data with class-transformer
NestJS integrates with class-transformer
to convert plain JavaScript objects into class instances. This allows you to use instance methods in your DTOs.
To apply transformation, you can use the plainToClass()
function from class-transformer
. Here’s an example of how to apply it manually:
typescriptCopyEditimport { plainToClass } from 'class-transformer';
import { CreateUserDto } from './dto/create-user.dto';
const userData = {
firstName: 'John',
lastName: 'Doe',
email: '[email protected]',
age: 25,
};
const createUserDto = plainToClass(CreateUserDto, userData);
This is particularly useful when you need to apply transformation explicitly before validation.
Creating Custom Validation Decorators
In some cases, you may need to create custom validation rules. Here’s an example of a custom validator for a “strong password”:
typescriptCopyEditimport { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';
export function IsStrongPassword(validationOptions?: ValidationOptions) {
return (object: Object, propertyName: string) => {
registerDecorator({
name: 'isStrongPassword',
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;
return typeof value === 'string' && passwordRegex.test(value);
},
defaultMessage(args: ValidationArguments) {
return 'Password must be at least 8 characters long and contain both letters and numbers';
},
},
});
};
}
You can then use this custom decorator in your DTOs:
typescriptCopyEditexport class CreateUserDto {
@IsStrongPassword({ message: 'Password must be strong' })
password: string;
}
Error Handling in Validation
When validation fails, NestJS will automatically return a 400 Bad Request
response with a detailed error message that highlights which fields failed validation.
For example, if the CreateUserDto
is invalid, you might get a response like this:
jsonCopyEdit{
"statusCode": 400,
"message": [
"First name must be a string",
"Email must be a valid email address"
],
"error": "Bad Request"
}
Best Practices for Validation
- Use Global Validation: Apply the
ValidationPipe
globally for consistency. - Use DTOs for All Incoming Requests: Always define DTOs for each type of incoming request to ensure data consistency.
- Provide Custom Error Messages: Use meaningful error messages to make debugging easier for clients.
- Use
class-transformer
for Complex Transformations: For advanced cases, useclass-transformer
to convert plain objects to class instances. - Handle Sensitive Data with Caution: Be careful about logging sensitive information that may appear in validation errors.
By implementing DTOs and validation with class-validator
, you can ensure that your NestJS application is both secure and robust. Proper validation ensures that only valid data enters your system, reducing the risk of bugs and vulnerabilities.