+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 212 of 355

📘 Validation: Class-Validator and Joi

Master validation: class-validator and joi in TypeScript with practical examples, best practices, and real-world applications 🚀

🚀Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript 📝
  • TypeScript installation ⚡
  • VS Code or preferred IDE 💻

What you'll learn

  • Understand validation fundamentals 🎯
  • Apply validation in real projects 🏗️
  • Debug common validation issues 🐛
  • Write type-safe validation code ✨

🎯 Introduction

Welcome to this exciting tutorial on validation in TypeScript! 🎉 In this guide, we’ll explore how to validate data using two powerful libraries: class-validator and Joi.

You’ll discover how proper validation can transform your TypeScript applications by catching errors before they cause problems. Whether you’re building REST APIs 🌐, processing user forms 📝, or handling data from external sources 📡, understanding validation is essential for writing robust, secure applications.

By the end of this tutorial, you’ll feel confident implementing bulletproof validation in your own projects! Let’s dive in! 🏊‍♂️

📚 Understanding Validation

🤔 What is Data Validation?

Validation is like having a security guard 🛡️ at the entrance of your application. Think of it as a checkpoint that examines incoming data and only allows clean, properly formatted information to pass through.

In TypeScript terms, validation ensures that data matches expected types and rules at runtime 🏃‍♂️. This means you can:

  • ✨ Catch invalid data before it corrupts your application
  • 🚀 Provide clear error messages to users
  • 🛡️ Protect against malicious input
  • 📖 Document your data requirements with code

💡 Why Use Validation Libraries?

Here’s why developers love validation libraries:

  1. Runtime Safety 🔒: TypeScript types disappear at runtime - validation stays
  2. Rich Error Messages 💻: Get detailed feedback about what went wrong
  3. Decorator Magic 📖: Clean, readable validation rules
  4. Async Support 🔧: Handle complex validation scenarios

Real-world example: Imagine building a user registration API 👤. With validation, you can ensure emails are properly formatted, passwords meet security requirements, and required fields aren’t missing!

🔧 Basic Syntax and Usage

📝 Class-Validator Example

Let’s start with a friendly class-validator example:

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

// 👋 Hello, class-validator!
class CreateUserDto {
  @IsNotEmpty({ message: "Name is required! 📝" })
  name: string;

  @IsEmail({}, { message: "Please provide a valid email 📧" })
  email: string;

  @MinLength(8, { message: "Password must be at least 8 characters 🔐" })
  password: string;

  @IsOptional()
  bio?: string; // 🎯 Optional field
}

💡 Explanation: Notice how decorators make validation rules crystal clear! Each decorator validates a specific aspect of the data.

🎨 Joi Example

Here’s the same validation using Joi:

import Joi from 'joi';

// 🎨 Creating a Joi schema
const userSchema = Joi.object({
  name: Joi.string()
    .required()
    .messages({ 'any.required': 'Name is required! 📝' }),
  
  email: Joi.string()
    .email()
    .required()
    .messages({ 'string.email': 'Please provide a valid email 📧' }),
  
  password: Joi.string()
    .min(8)
    .required()
    .messages({ 'string.min': 'Password must be at least 8 characters 🔐' }),
  
  bio: Joi.string().optional() // 🎯 Optional field
});

// 🔄 Using the schema
const validateUser = (userData: any) => {
  const { error, value } = userSchema.validate(userData);
  if (error) {
    console.log("❌ Validation failed:", error.details);
    return null;
  }
  return value; // ✅ Clean, validated data
};

💡 Practical Examples

🛒 Example 1: E-commerce Product Validation

Let’s build something real with class-validator:

import { 
  IsNotEmpty, 
  IsNumber, 
  Min, 
  Max, 
  IsArray, 
  ValidateNested,
  IsEnum,
  IsUrl
} from 'class-validator';
import { Type } from 'class-transformer';

// 🏷️ Product category enum
enum ProductCategory {
  ELECTRONICS = 'electronics',
  CLOTHING = 'clothing',
  BOOKS = 'books',
  HOME = 'home'
}

// 📦 Product dimension validation
class ProductDimensions {
  @IsNumber({}, { message: "Width must be a number 📏" })
  @Min(0.1, { message: "Width must be at least 0.1cm" })
  width: number;

  @IsNumber({}, { message: "Height must be a number 📏" })
  @Min(0.1, { message: "Height must be at least 0.1cm" })
  height: number;

  @IsNumber({}, { message: "Depth must be a number 📏" })
  @Min(0.1, { message: "Depth must be at least 0.1cm" })
  depth: number;
}

// 🛍️ Main product class
class Product {
  @IsNotEmpty({ message: "Product name is required! 🛍️" })
  name: string;

  @IsNotEmpty({ message: "Description helps customers! 📝" })
  description: string;

  @IsNumber({}, { message: "Price must be a number 💰" })
  @Min(0.01, { message: "Price must be at least $0.01" })
  @Max(10000, { message: "Price cannot exceed $10,000" })
  price: number;

  @IsEnum(ProductCategory, { 
    message: "Category must be one of: electronics, clothing, books, home 🏷️" 
  })
  category: ProductCategory;

  @IsArray({ message: "Tags should be an array 🏷️" })
  tags: string[];

  @IsUrl({}, { message: "Please provide a valid image URL 🖼️" })
  imageUrl: string;

  @ValidateNested()
  @Type(() => ProductDimensions)
  dimensions: ProductDimensions;
}

// 🎮 Let's use it!
import { validate } from 'class-validator';

const createProduct = async (productData: any) => {
  const product = Object.assign(new Product(), productData);
  
  const errors = await validate(product);
  
  if (errors.length > 0) {
    console.log("❌ Product validation failed:");
    errors.forEach(error => {
      console.log(`  🚫 ${error.property}: ${Object.values(error.constraints || {}).join(', ')}`);
    });
    return null;
  }
  
  console.log("✅ Product is valid! Ready to save 🎉");
  return product;
};

🎯 Try it yourself: Add inventory tracking with stock quantity validation!

🎮 Example 2: Game Character Creation with Joi

Let’s make it fun with Joi:

import Joi from 'joi';

// 🎮 Character stats schema
const characterStatsSchema = Joi.object({
  strength: Joi.number().min(1).max(100).required(),
  intelligence: Joi.number().min(1).max(100).required(),
  agility: Joi.number().min(1).max(100).required(),
  vitality: Joi.number().min(1).max(100).required()
}).custom((value, helpers) => {
  // 🎯 Total stats cannot exceed 200 points
  const total = value.strength + value.intelligence + value.agility + value.vitality;
  if (total > 200) {
    return helpers.error('stats.total', { total });
  }
  return value;
}).messages({
  'stats.total': 'Total stats cannot exceed 200 points! Currently: {{#total}} 🎯'
});

// 🏆 Character creation schema
const characterSchema = Joi.object({
  name: Joi.string()
    .min(3)
    .max(20)
    .pattern(/^[a-zA-Z\s]+$/)
    .required()
    .messages({
      'string.pattern.base': 'Character name can only contain letters and spaces 🔤',
      'string.min': 'Name must be at least 3 characters long 📏',
      'string.max': 'Name cannot exceed 20 characters 📏'
    }),

  class: Joi.string()
    .valid('warrior', 'mage', 'archer', 'rogue')
    .required()
    .messages({
      'any.only': 'Character class must be: warrior, mage, archer, or rogue ⚔️'
    }),

  level: Joi.number()
    .integer()
    .min(1)
    .max(100)
    .default(1)
    .messages({
      'number.min': 'Character level must be at least 1 🌟',
      'number.max': 'Character level cannot exceed 100 🏆'
    }),

  stats: characterStatsSchema,

  equipment: Joi.array()
    .items(Joi.string())
    .max(10)
    .default([])
    .messages({
      'array.max': 'Character can carry maximum 10 items 🎒'
    }),

  backstory: Joi.string()
    .max(500)
    .optional()
    .messages({
      'string.max': 'Backstory cannot exceed 500 characters 📖'
    })
});

// 🎯 Character creation function
const createCharacter = (characterData: any) => {
  const { error, value } = characterSchema.validate(characterData, {
    abortEarly: false, // Show all errors
    stripUnknown: true // Remove unknown fields
  });

  if (error) {
    console.log("❌ Character creation failed:");
    error.details.forEach(detail => {
      console.log(`  🚫 ${detail.path.join('.')}: ${detail.message}`);
    });
    return null;
  }

  console.log(`✅ Welcome, ${value.name} the ${value.class}! 🎉`);
  return value;
};

// 🎮 Test character creation
const heroData = {
  name: "Aragorn",
  class: "warrior",
  stats: {
    strength: 80,
    intelligence: 40,
    agility: 50,
    vitality: 30
  },
  equipment: ["sword", "shield", "healing potion"],
  backstory: "A brave warrior from the northern kingdoms"
};

const character = createCharacter(heroData);

🚀 Advanced Concepts

🧙‍♂️ Custom Validators with Class-Validator

When you’re ready to level up, create custom validation logic:

import { 
  registerDecorator, 
  ValidationOptions, 
  ValidatorConstraint, 
  ValidatorConstraintInterface,
  ValidationArguments 
} from 'class-validator';

// 🎯 Custom password strength validator
@ValidatorConstraint({ name: 'isStrongPassword', async: false })
export class IsStrongPasswordConstraint implements ValidatorConstraintInterface {
  validate(password: string, args: ValidationArguments) {
    // 🔐 Password must contain: uppercase, lowercase, number, special char
    const hasUpper = /[A-Z]/.test(password);
    const hasLower = /[a-z]/.test(password);
    const hasNumber = /\d/.test(password);
    const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
    
    return hasUpper && hasLower && hasNumber && hasSpecial;
  }

  defaultMessage(args: ValidationArguments) {
    return 'Password must contain uppercase, lowercase, number, and special character 🔐';
  }
}

// 🪄 Decorator function
export function IsStrongPassword(validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      constraints: [],
      validator: IsStrongPasswordConstraint,
    });
  };
}

// 👤 Using the custom validator
class SecureUser {
  @IsNotEmpty()
  username: string;

  @IsStrongPassword({ message: "Create a stronger password! 💪" })
  password: string;
}

🏗️ Advanced Joi Schemas

For the brave developers, here’s schema composition:

// 🚀 Base schemas for reuse
const emailSchema = Joi.string().email().required();
const phoneSchema = Joi.string().pattern(/^\+?[1-9]\d{1,14}$/).required();

// 📱 Contact schema
const contactSchema = Joi.object({
  email: emailSchema,
  phone: phoneSchema.optional(),
  preferredContact: Joi.string().valid('email', 'phone').default('email')
}).when(Joi.object({ phone: Joi.exist() }), {
  then: Joi.object({
    preferredContact: Joi.required()
  })
});

// 🏢 Company schema with conditional validation
const companySchema = Joi.object({
  name: Joi.string().required(),
  type: Joi.string().valid('startup', 'enterprise', 'nonprofit').required(),
  employees: Joi.number().integer().min(1).required(),
  
  // 💰 Revenue required for startups and enterprises
  revenue: Joi.when('type', {
    is: Joi.valid('startup', 'enterprise'),
    then: Joi.number().min(0).required(),
    otherwise: Joi.forbidden()
  }),
  
  // 🎯 Mission statement required for nonprofits
  mission: Joi.when('type', {
    is: 'nonprofit',
    then: Joi.string().required(),
    otherwise: Joi.forbidden()
  })
});

⚠️ Common Pitfalls and Solutions

😱 Pitfall 1: Forgetting Async Validation

// ❌ Wrong way - not awaiting validation!
const errors = validate(user); // 💥 Returns Promise!
if (errors.length > 0) {
  // This won't work as expected
}

// ✅ Correct way - await the validation!
const errors = await validate(user); // ✅ Wait for validation
if (errors.length > 0) {
  console.log("⚠️ Validation errors found!");
  // Handle errors properly
}

🤯 Pitfall 2: Not Transforming Input Data

// ❌ Dangerous - plain object without validation!
const createUser = (userData: any) => {
  const user = userData; // 💥 No validation or transformation!
  return saveUser(user);
};

// ✅ Safe - transform and validate!
import { plainToClass } from 'class-transformer';

const createUser = async (userData: any) => {
  // 🎨 Transform plain object to class instance
  const user = plainToClass(CreateUserDto, userData);
  
  // 🛡️ Validate the transformed object
  const errors = await validate(user);
  
  if (errors.length > 0) {
    throw new Error("Validation failed! ❌");
  }
  
  return saveUser(user); // ✅ Safe to save!
};

🔥 Pitfall 3: Poor Error Messaging

// ❌ Unhelpful error messages
@IsNotEmpty()
password: string;

// ✅ Clear, friendly error messages!
@IsNotEmpty({ message: "Password is required for security! 🔐" })
@MinLength(8, { message: "Password must be at least 8 characters long 📏" })
password: string;

🛠️ Best Practices

  1. 🎯 Be Specific: Use detailed validation rules and clear error messages
  2. 📝 Document with Validation: Let validation rules serve as documentation
  3. 🛡️ Validate Early: Check data at API boundaries and form inputs
  4. 🎨 Use Custom Validators: Create reusable validation logic for complex rules
  5. ✨ Keep It Simple: Don’t over-complicate validation schemas
  6. 🔄 Test Your Validation: Write unit tests for validation logic
  7. 🌐 Consider Internationalization: Make error messages translatable

🧪 Hands-On Exercise

🎯 Challenge: Build a Blog Post Validation System

Create a comprehensive validation system for a blogging platform:

📋 Requirements:

  • ✅ Post title (3-100 characters, no special characters in titles)
  • 📝 Content (minimum 50 characters, maximum 5000)
  • 🏷️ Tags (1-10 tags, each 2-20 characters)
  • 👤 Author information (name and email)
  • 📅 Publishing date (cannot be in the past)
  • 🎨 Category selection from predefined list
  • 🔗 Optional featured image URL

🚀 Bonus Points:

  • Add custom validator for profanity filtering
  • Implement slug generation and validation
  • Create read time estimation
  • Add SEO meta description validation

💡 Solution

🔍 Click to see solution
import { 
  IsNotEmpty, 
  IsString, 
  Length, 
  IsArray, 
  ArrayMinSize, 
  ArrayMaxSize,
  IsEmail,
  IsUrl,
  IsEnum,
  IsDate,
  MinDate,
  ValidateNested,
  Matches
} from 'class-validator';
import { Type } from 'class-transformer';

// 📚 Blog categories
enum BlogCategory {
  TECH = 'technology',
  LIFESTYLE = 'lifestyle',
  TRAVEL = 'travel',
  FOOD = 'food',
  BUSINESS = 'business'
}

// 👤 Author information
class Author {
  @IsNotEmpty({ message: "Author name is required! ✍️" })
  @Length(2, 50, { message: "Author name must be 2-50 characters 📏" })
  name: string;

  @IsEmail({}, { message: "Valid email required for author 📧" })
  email: string;
}

// 📝 Main blog post validation
class BlogPost {
  @IsNotEmpty({ message: "Title is required! 📰" })
  @Length(3, 100, { message: "Title must be 3-100 characters 📏" })
  @Matches(/^[a-zA-Z0-9\s\-.,!?]+$/, { 
    message: "Title can only contain letters, numbers, spaces, and basic punctuation ✏️" 
  })
  title: string;

  @IsNotEmpty({ message: "Content is required! 📖" })
  @Length(50, 5000, { 
    message: "Content must be between 50-5000 characters (current: $value.length) 📏" 
  })
  content: string;

  @IsArray({ message: "Tags must be an array 🏷️" })
  @ArrayMinSize(1, { message: "At least 1 tag is required 🏷️" })
  @ArrayMaxSize(10, { message: "Maximum 10 tags allowed 🏷️" })
  @IsString({ each: true, message: "Each tag must be a string 📝" })
  @Length(2, 20, { each: true, message: "Each tag must be 2-20 characters 📏" })
  tags: string[];

  @ValidateNested()
  @Type(() => Author)
  author: Author;

  @IsDate({ message: "Valid publish date required 📅" })
  @MinDate(new Date(), { message: "Publish date cannot be in the past ⏰" })
  @Type(() => Date)
  publishDate: Date;

  @IsEnum(BlogCategory, { 
    message: "Category must be: technology, lifestyle, travel, food, or business 📂" 
  })
  category: BlogCategory;

  @IsUrl({}, { message: "Featured image must be a valid URL 🖼️" })
  @IsOptional()
  featuredImage?: string;

  @Length(50, 160, { message: "Meta description must be 50-160 characters for SEO 🔍" })
  @IsOptional()
  metaDescription?: string;
}

// 🎯 Validation function with detailed feedback
const validateBlogPost = async (postData: any): Promise<BlogPost | null> => {
  const post = Object.assign(new BlogPost(), {
    ...postData,
    publishDate: new Date(postData.publishDate)
  });

  const errors = await validate(post, { 
    whitelist: true, // Remove unknown properties
    forbidNonWhitelisted: true // Reject unknown properties
  });

  if (errors.length > 0) {
    console.log("❌ Blog post validation failed:");
    errors.forEach(error => {
      const field = error.property;
      const messages = Object.values(error.constraints || {});
      console.log(`  🚫 ${field}: ${messages.join(', ')}`);
    });
    return null;
  }

  // 🎉 Calculate reading time (average 200 words per minute)
  const wordCount = post.content.split(' ').length;
  const readTime = Math.ceil(wordCount / 200);
  
  console.log("✅ Blog post validation successful! 🎉");
  console.log(`📊 Stats: ${wordCount} words, ~${readTime} min read`);
  
  return post;
};

// 🧪 Test the validation
const testPost = {
  title: "TypeScript Validation: A Complete Guide",
  content: "This is a comprehensive guide to TypeScript validation using class-validator and Joi. We'll explore various validation techniques, best practices, and real-world examples to help you master data validation in your TypeScript applications. Validation is crucial for maintaining data integrity and providing great user experiences.",
  tags: ["typescript", "validation", "tutorial", "programming"],
  author: {
    name: "Jane Developer",
    email: "[email protected]"
  },
  publishDate: new Date(Date.now() + 24 * 60 * 60 * 1000), // Tomorrow
  category: BlogCategory.TECH,
  featuredImage: "https://example.com/featured-image.jpg",
  metaDescription: "Learn TypeScript validation with class-validator and Joi in this comprehensive tutorial with practical examples."
};

// 🚀 Run validation
validateBlogPost(testPost).then(result => {
  if (result) {
    console.log("🎊 Ready to publish!", result.title);
  }
});

🎓 Key Takeaways

You’ve learned so much! Here’s what you can now do:

  • Create robust validation with class-validator and Joi 💪
  • Avoid common validation mistakes that trip up developers 🛡️
  • Apply validation best practices in real projects 🎯
  • Debug validation issues like a pro 🐛
  • Build secure, reliable applications with TypeScript! 🚀

Remember: Validation is your first line of defense against bad data. It’s not just about preventing crashes - it’s about creating amazing user experiences! 🤝

🤝 Next Steps

Congratulations! 🎉 You’ve mastered TypeScript validation!

Here’s what to do next:

  1. 💻 Practice with the exercises above
  2. 🏗️ Add validation to an existing project
  3. 📚 Move on to our next tutorial: Error Handling and Logging
  4. 🌟 Share your validation wins with the community!

Remember: Every secure application starts with solid validation. Keep coding, keep validating, and most importantly, have fun building bulletproof apps! 🚀


Happy validating! 🎉🛡️✨