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:
- Runtime Safety 🔒: TypeScript types disappear at runtime - validation stays
- Rich Error Messages 💻: Get detailed feedback about what went wrong
- Decorator Magic 📖: Clean, readable validation rules
- 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
- 🎯 Be Specific: Use detailed validation rules and clear error messages
- 📝 Document with Validation: Let validation rules serve as documentation
- 🛡️ Validate Early: Check data at API boundaries and form inputs
- 🎨 Use Custom Validators: Create reusable validation logic for complex rules
- ✨ Keep It Simple: Don’t over-complicate validation schemas
- 🔄 Test Your Validation: Write unit tests for validation logic
- 🌐 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:
- 💻 Practice with the exercises above
- 🏗️ Add validation to an existing project
- 📚 Move on to our next tutorial: Error Handling and Logging
- 🌟 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! 🎉🛡️✨