class-validator
Library
class-validator
Overview
class-validator is a TypeScript-specific decorator-based validation library. It enables declarative validation by applying decorators to class properties and is adopted as the default validator in NestJS. Internally using validator.js, it works in both Node.js and browsers. As of 2025, with TypeScript maturity and NestJS popularity, adoption in enterprise TypeScript projects is expanding, offering a validation solution with high affinity for object-oriented programming.
Details
class-validator 0.14 series is the latest stable version as of 2025, a validation library that maximally leverages the power of TypeScript decorators. It internally uses the validator.js library, providing rich built-in validation functions. Through standard adoption in the NestJS framework, it holds an important position in enterprise Node.js application development. Integration with the class-transformer library enables unified execution of conversion from plain objects to class instances and validation.
Key Features
- Decorator-based: Declarative validation through TypeScript decorators
- Type Safety: Strong type support through complete TypeScript integration
- NestJS Integration: Enterprise-grade application support through standard adoption in NestJS
- Rich Built-in Constraints: Over 60 validators including @IsEmail, @IsNumber
- Custom Validators: Extensibility for custom business logic
- Nested Validation: Validation support for object hierarchical structures
Pros and Cons
Pros
- Natural and intuitive API for TypeScript developers
- Stability through standard position in NestJS ecosystem
- High affinity with object-oriented programming
- Powerful data processing through integration with class-transformer
- Detailed error information and customizability
- Cross-platform support for Node.js and browsers
Cons
- TypeScript/JavaScript only with no cross-language support
- Configuration complexity due to decorator dependencies
- Verbosity requiring validation class definitions
- Runtime performance overhead
- Not suitable for functional programming style
- Class management complexity in large projects
Reference Pages
Code Examples
Installation and Basic Setup
# Installing class-validator
npm install class-validator
yarn add class-validator
pnpm add class-validator
# reflect-metadata required (for TypeScript decorators)
npm install reflect-metadata
# Recommended use with class-transformer
npm install class-transformer
# TypeScript configuration required
# tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Basic Class Validation
import {
validate,
validateOrReject,
Contains,
IsInt,
Length,
IsEmail,
IsFQDN,
IsDate,
Min,
Max,
IsOptional,
IsString,
IsNumber,
IsBoolean
} from 'class-validator';
// Import reflect-metadata at entry point
import 'reflect-metadata';
export class User {
@Length(10, 20, {
message: 'Title must be between 10 and 20 characters'
})
title: string;
@Contains('hello', {
message: 'Text must contain "hello"'
})
text: string;
@IsInt({ message: 'Rating must be an integer' })
@Min(0, { message: 'Rating must be at least 0' })
@Max(10, { message: 'Rating must be at most 10' })
rating: number;
@IsEmail({}, { message: 'Please enter a valid email address' })
email: string;
@IsFQDN({}, { message: 'Please enter a valid domain name' })
site: string;
@IsDate({ message: 'Please enter a valid date' })
createDate: Date;
@IsOptional()
@IsString({ message: 'Description must be a string' })
description?: string;
constructor() {
this.title = '';
this.text = '';
this.rating = 0;
this.email = '';
this.site = '';
this.createDate = new Date();
}
}
// Validation execution example
async function validateUser() {
const user = new User();
user.title = 'Hello'; // Less than 10 characters, will error
user.text = 'world'; // Doesn't contain 'hello', will error
user.rating = 15; // Exceeds 10, will error
user.email = 'invalid-email'; // Invalid email format
user.site = 'invalid-domain'; // Invalid domain
user.createDate = new Date();
try {
const errors = await validate(user);
if (errors.length > 0) {
console.log('Validation errors:');
errors.forEach(error => {
console.log(`Property: ${error.property}`);
console.log(`Value: ${error.value}`);
console.log(`Constraints:`, error.constraints);
console.log('---');
});
} else {
console.log('Validation successful');
}
} catch (errors) {
console.log('Validation exception:', errors);
}
}
// Exception handling using validateOrReject
async function validateUserWithException() {
const user = new User();
// Set invalid data
try {
await validateOrReject(user);
console.log('Validation successful');
} catch (errors) {
console.log('Validation errors:', errors);
}
}
Nested Objects and Array Validation
import {
ValidateNested,
IsArray,
ArrayMinSize,
ArrayMaxSize,
ArrayNotEmpty,
IsInstance,
Type
} from 'class-validator';
export class Address {
@Length(1, 50, { message: 'Street must be between 1 and 50 characters' })
street: string;
@Length(1, 50, { message: 'City must be between 1 and 50 characters' })
city: string;
@Length(1, 50, { message: 'State must be between 1 and 50 characters' })
state: string;
@Length(5, 10, { message: 'Zip code must be between 5 and 10 characters' })
zipCode: string;
constructor() {
this.street = '';
this.city = '';
this.state = '';
this.zipCode = '';
}
}
export class Tag {
@Length(1, 20, { message: 'Tag name must be between 1 and 20 characters' })
name: string;
@IsOptional()
@IsString({ message: 'Color must be a string' })
color?: string;
constructor(name: string = '', color?: string) {
this.name = name;
this.color = color;
}
}
export class Post {
@Length(10, 20)
title: string;
@Contains('hello')
text: string;
@IsInt()
@Min(0)
@Max(10)
rating: number;
// Nested object validation
@ValidateNested()
@IsInstance(Address, { message: 'Address must be a valid Address instance' })
@Type(() => Address)
address: Address;
// Array element validation
@IsArray({ message: 'Tags must be an array' })
@ValidateNested({ each: true })
@ArrayMinSize(1, { message: 'At least one tag is required' })
@ArrayMaxSize(5, { message: 'Maximum 5 tags allowed' })
@Type(() => Tag)
tags: Tag[];
// Primitive array validation
@IsArray()
@ArrayNotEmpty({ message: 'Categories cannot be empty' })
@IsString({ each: true, message: 'Each category must be a string' })
categories: string[];
constructor() {
this.title = '';
this.text = '';
this.rating = 0;
this.address = new Address();
this.tags = [];
this.categories = [];
}
}
// Complex object validation example
async function validateComplexObject() {
const post = new Post();
post.title = 'Hello World!';
post.text = 'This is a hello world post';
post.rating = 8;
// Address setup
post.address.street = '123 Main St';
post.address.city = 'New York';
post.address.state = 'NY';
post.address.zipCode = '10001';
// Tags setup
post.tags = [
new Tag('programming', 'blue'),
new Tag('typescript', 'green')
];
post.categories = ['tech', 'programming'];
const errors = await validate(post);
if (errors.length === 0) {
console.log('All validations successful');
} else {
console.log('Validation errors:', errors);
}
}
NestJS Integration and DTO Validation
// DTO (Data Transfer Object) definition in NestJS
import { IsString, IsNumber, IsEmail, IsOptional, Min, Max } from 'class-validator';
import { Transform, Type } from 'class-transformer';
export class CreateUserDto {
@IsString({ message: 'Name must be a string' })
@Length(2, 50, { message: 'Name must be between 2 and 50 characters' })
name: string;
@IsEmail({}, { message: 'Please enter a valid email address' })
email: string;
@IsNumber({}, { message: 'Age must be a number' })
@Min(18, { message: 'Age must be at least 18' })
@Max(120, { message: 'Age must be at most 120' })
@Type(() => Number) // String to number conversion
age: number;
@IsOptional()
@IsString()
@Transform(({ value }) => value?.trim()) // Remove leading/trailing whitespace
bio?: string;
}
export class UpdateUserDto {
@IsOptional()
@IsString({ message: 'Name must be a string' })
@Length(2, 50, { message: 'Name must be between 2 and 50 characters' })
name?: string;
@IsOptional()
@IsEmail({}, { message: 'Please enter a valid email address' })
email?: string;
@IsOptional()
@IsNumber({}, { message: 'Age must be a number' })
@Min(18, { message: 'Age must be at least 18' })
@Max(120, { message: 'Age must be at most 120' })
@Type(() => Number)
age?: number;
}
// NestJS Controller
import { Controller, Post, Body, Put, Param, ParseIntPipe } from '@nestjs/common';
import { ValidationPipe } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Post()
async createUser(@Body() createUserDto: CreateUserDto) {
// Validation is automatically executed
console.log('Received data:', createUserDto);
return { message: 'User created successfully', user: createUserDto };
}
@Put(':id')
async updateUser(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto
) {
console.log(`Updating user ${id}:`, updateUserDto);
return { message: 'User updated successfully', user: updateUserDto };
}
}
// Global validation setup in NestJS main.ts
import { 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({
whitelist: true, // Remove properties not defined in DTO
forbidNonWhitelisted: true, // Error if undefined properties exist
transform: true, // Automatically convert to DTO instances
disableErrorMessages: false, // Show error messages
validationError: {
target: false, // Don't include target object in error response
value: false // Don't include value in error response
}
}));
await app.listen(3000);
}
bootstrap();
Custom Validators and Advanced Validation
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
Validate,
IsDateString
} from 'class-validator';
// Custom validator: Password strength check
@ValidatorConstraint({ name: 'isStrongPassword', async: false })
export class IsStrongPasswordConstraint implements ValidatorConstraintInterface {
validate(password: string, args: ValidationArguments) {
if (!password) return false;
// 8+ characters, uppercase, lowercase, digit, special character
const strongPasswordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
return strongPasswordRegex.test(password);
}
defaultMessage(args: ValidationArguments) {
return 'Password must be at least 8 characters and contain uppercase, lowercase, digit, and special character';
}
}
// Custom decorator creation
export function IsStrongPassword(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsStrongPasswordConstraint,
});
};
}
// Async validator: Username uniqueness check
@ValidatorConstraint({ name: 'isUserNameUnique', async: true })
export class IsUserNameUniqueConstraint implements ValidatorConstraintInterface {
async validate(userName: string, args: ValidationArguments) {
// In real applications, check database
const existingUsers = ['admin', 'user', 'test']; // Mock data
return new Promise((resolve) => {
setTimeout(() => {
resolve(!existingUsers.includes(userName.toLowerCase()));
}, 100); // Simulate async processing
});
}
defaultMessage(args: ValidationArguments) {
return 'Username "$value" is already taken';
}
}
export function IsUserNameUnique(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsUserNameUniqueConstraint,
});
};
}
// Conditional validation
@ValidatorConstraint({ name: 'isMatchingPassword', async: false })
export class IsMatchingPasswordConstraint implements ValidatorConstraintInterface {
validate(confirmPassword: string, args: ValidationArguments) {
const object = args.object as any;
return confirmPassword === object.password;
}
defaultMessage(args: ValidationArguments) {
return 'Passwords do not match';
}
}
export function IsMatchingPassword(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsMatchingPasswordConstraint,
});
};
}
// Class using advanced validation
export class AdvancedUser {
@IsString({ message: 'Username must be a string' })
@Length(3, 20, { message: 'Username must be between 3 and 20 characters' })
@IsUserNameUnique({ message: 'This username is already taken' })
userName: string;
@IsEmail({}, { message: 'Please enter a valid email address' })
email: string;
@IsStrongPassword({ message: 'Please set a stronger password' })
password: string;
@IsString({ message: 'Password confirmation must be a string' })
@IsMatchingPassword({ message: 'Passwords do not match' })
confirmPassword: string;
@IsOptional()
@IsDateString({}, { message: 'Please enter a valid date format (YYYY-MM-DD)' })
birthDate?: string;
constructor() {
this.userName = '';
this.email = '';
this.password = '';
this.confirmPassword = '';
}
}
// Testing custom validators
async function testAdvancedValidation() {
const user = new AdvancedUser();
user.userName = 'newuser';
user.email = '[email protected]';
user.password = 'StrongPass123!';
user.confirmPassword = 'StrongPass123!';
user.birthDate = '1990-01-01';
try {
const errors = await validate(user);
if (errors.length === 0) {
console.log('Advanced validation successful');
} else {
console.log('Validation errors:', errors);
}
} catch (error) {
console.error('Validation exception:', error);
}
}
Error Handling and Message Customization
import { validate, ValidationError } from 'class-validator';
// Detailed error analysis
function formatValidationErrors(errors: ValidationError[]): any {
const formattedErrors: any = {};
errors.forEach(error => {
const property = error.property;
if (error.constraints) {
formattedErrors[property] = Object.values(error.constraints);
}
// Handle nested errors
if (error.children && error.children.length > 0) {
formattedErrors[property] = {
...formattedErrors[property],
children: formatValidationErrors(error.children)
};
}
});
return formattedErrors;
}
// Custom error response generation
class ValidationErrorResponse {
message: string;
statusCode: number;
errors: any;
timestamp: string;
constructor(errors: ValidationError[]) {
this.message = 'Validation errors occurred';
this.statusCode = 400;
this.errors = formatValidationErrors(errors);
this.timestamp = new Date().toISOString();
}
}
// Global error handler (for Express.js)
import { Request, Response, NextFunction } from 'express';
export function validationErrorHandler(
error: any,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof Array && error[0] instanceof ValidationError) {
const validationResponse = new ValidationErrorResponse(error);
return res.status(400).json(validationResponse);
}
next(error);
}
// Validation function wrapper
export async function validateAndFormat<T>(
object: T,
options?: {
skipMissingProperties?: boolean;
whitelist?: boolean;
forbidNonWhitelisted?: boolean;
}
): Promise<{ isValid: boolean; errors?: any; data?: T }> {
try {
const errors = await validate(object as any, options);
if (errors.length > 0) {
return {
isValid: false,
errors: formatValidationErrors(errors)
};
}
return {
isValid: true,
data: object
};
} catch (error) {
return {
isValid: false,
errors: { global: ['An unexpected error occurred'] }
};
}
}
// Usage example
async function handleUserRegistration(userData: any) {
const user = Object.assign(new AdvancedUser(), userData);
const result = await validateAndFormat(user);
if (!result.isValid) {
console.log('Validation failed:', result.errors);
return { success: false, errors: result.errors };
}
console.log('Validation successful:', result.data);
return { success: true, user: result.data };
}
// Partial validation (specific properties only)
async function validateSpecificProperty<T>(
object: T,
propertyName: string
): Promise<string[]> {
const errors = await validate(object as any, {
skipMissingProperties: true
});
const propertyErrors = errors.find(error => error.property === propertyName);
if (propertyErrors && propertyErrors.constraints) {
return Object.values(propertyErrors.constraints);
}
return [];
}
// Real-time validation (for forms)
export class FormValidator<T> {
private object: T;
private fieldErrors: Map<string, string[]> = new Map();
constructor(objectType: new () => T) {
this.object = new objectType();
}
async validateField(fieldName: string, value: any): Promise<string[]> {
(this.object as any)[fieldName] = value;
const errors = await validateSpecificProperty(this.object, fieldName);
this.fieldErrors.set(fieldName, errors);
return errors;
}
getFieldErrors(fieldName: string): string[] {
return this.fieldErrors.get(fieldName) || [];
}
async validateAll(): Promise<{ isValid: boolean; errors: Map<string, string[]> }> {
const errors = await validate(this.object as any);
this.fieldErrors.clear();
errors.forEach(error => {
if (error.constraints) {
this.fieldErrors.set(error.property, Object.values(error.constraints));
}
});
return {
isValid: errors.length === 0,
errors: this.fieldErrors
};
}
}