Validation Libraries: Joi vs Yup vs Zod
Learn: Validation Libraries: Joi vs Yup vs Zod
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
Validation Libraries: Joi vs Yup vs Zod
Problem
Modern applications require robust data validation across multiple layers—API inputs, form submissions, database operations, and configuration files. Choosing the right validation library impacts code maintainability, bundle size, type safety, and developer experience.
Key Challenges:
- Type safety and TypeScript integration
- Bundle size and performance
- API ergonomics and learning curve
- Error message customization
- Async validation support
- Schema reusability
Solution Overview
| Aspect | Joi | Yup | Zod |
| Bundle Size | ~90KB | ~15KB | ~8KB |
| TypeScript | Partial | Good | Excellent |
| Type Inference | Manual | Manual | Automatic |
| Learning Curve | Steep | Moderate | Gentle |
| Async Support | Yes | Yes | Yes |
| Best For | Enterprise | React Forms | Modern TS Apps |
Code Examples
1. Joi - Enterprise-Grade Validation
Use Case: Large applications with complex validation rules
const Joi = require('joi');
// Define schema
const userSchema = Joi.object({
email: Joi.string()
.email({ tlds: { allow: false } })
.required()
.messages({
'string.email': 'Please provide a valid email',
'any.required': 'Email is required'
}),
password: Joi.string()
.min(8)
.pattern(/^(?=.*[A-Z])(?=.*\d)/)
.required()
.messages({
'string.pattern.base': 'Password must contain uppercase and number'
}),
age: Joi.number()
.integer()
.min(18)
.max(120)
.optional(),
role: Joi.string()
.valid('admin', 'user', 'guest')
.default('user'),
profile: Joi.object({
firstName: Joi.string().required(),
lastName: Joi.string().required(),
bio: Joi.string().max(500)
}).optional()
});
// Validation
const data = {
email: 'user@example.com',
password: 'SecurePass123',
role: 'admin',
profile: {
firstName: 'John',
lastName: 'Doe'
}
};
const { error, value } = userSchema.validate(data);
if (error) {
console.error('Validation failed:', error.details);
} else {
console.log('Valid data:', value);
}
// Async validation with external checks
const advancedSchema = Joi.object({
username: Joi.string()
.alphanum()
.min(3)
.max(30)
.required()
.external(async (value) => {
const exists = await checkUsernameInDatabase(value);
if (exists) {
throw new Error('Username already taken');
}
})
});
Pros:
- Comprehensive validation rules
- Excellent for complex enterprise schemas
- Strong error customization
Cons:
- Largest bundle size
- Verbose syntax
- Weak TypeScript inference
2. Yup - React Forms Favorite
Use Case: React applications with Formik integration
import * as yup from 'yup';
// Define schema
const registrationSchema = yup.object().shape({
email: yup
.string()
.email('Invalid email format')
.required('Email is required'),
password: yup
.string()
.min(8, 'Password must be at least 8 characters')
.matches(
/^(?=.*[A-Z])(?=.*\d)/,
'Must contain uppercase letter and number'
)
.required('Password is required'),
confirmPassword: yup
.string()
.oneOf([yup.ref('password')], 'Passwords must match')
.required('Confirm password is required'),
age: yup
.number()
.typeError('Age must be a number')
.min(18, 'Must be 18 or older')
.max(120, 'Invalid age'),
terms: yup
.boolean()
.oneOf([true], 'You must accept terms')
.required()
});
// Validation
async function validateUser(data) {
try {
const validData = await registrationSchema.validate(data, {
abortEarly: false // Get all errors, not just first
});
console.log('Valid:', validData);
} catch (error) {
console.error('Errors:', error.inner);
// error.inner = [{ path: 'email', message: '...' }, ...]
}
}
// React + Formik Integration
import { Formik, Form, Field, ErrorMessage } from 'formik';
function RegistrationForm() {
return (
<Formik
initialValues={{
email: '',
password: '',
confirmPassword: '',
age: '',
terms: false
}}
validationSchema={registrationSchema}
onSubmit={(values) => {
console.log('Submitting:', values);
}}
>
{({ errors, touched }) => (
<Form>
<Field name="email" type="email" />
<ErrorMessage name="email" />
<Field name="password" type="password" />
<ErrorMessage name="password" />
<button type="submit">Register</button>
</Form>
)}
</Formik>
);
}
// Conditional validation
const conditionalSchema = yup.object().shape({
userType: yup.string().oneOf(['individual', 'business']),
businessName: yup
.string()
.when('userType', {
is: 'business',
then: yup.string().required('Business name required'),
otherwise: yup.string().notRequired()
})
});
Pros:
- Perfect Formik integration
- Smaller than Joi
- Good error handling
Cons:
- Manual TypeScript types
- No automatic type inference
- Moderate bundle size
3. Zod - Modern TypeScript First
Use Case: TypeScript projects requiring type safety
import { z } from 'zod';
// Define schema with automatic type inference
const userSchema = z.object({
email: z
.string()
.email('Invalid email')
.describe('User email address'),
password: z
.string()
.min(8, 'Minimum 8 characters')
.regex(/^(?=.*[A-Z])(?=.*\d)/, 'Must contain uppercase and number'),
age: z
.number()
.int()
.min(18)
.max(120)
.optional(),
role: z
.enum(['admin', 'user', 'guest'])
.default('user'),
profile: z.object({
firstName: z.string(),
lastName: z.string(),
bio: z.string().max(500).optional()
}).optional()
});
// Automatic type inference
type User = z.infer<typeof userSchema>;
// type User = {
// email: string;
// password: string;
// age?: number;
// role: 'admin' | 'user' | 'guest';
// profile?: { firstName: string; lastName: string; bio?: string };
// }
// Validation
const data = {
email: 'user@example.com',
password: 'SecurePass123',
role: 'admin' as const
};
try {
const validUser: User = userSchema.parse(data);
console.log('Valid:', validUser);
} catch (error) {
if (error instanceof z.ZodError) {
console.error('Validation errors:', error.errors);
// error.errors = [{ code, path, message }, ...]
}
}
// Safe parsing (no throw)
const result = userSchema.safeParse(data);
if (result.success) {
console.log('Valid:', result.data);
} else {
console.error('Errors:', result.error.flatten());
}
// Async validation
const advancedSchema = z.object({
username: z
.string()
.min(3)
.refine(
async (val) => !(await checkUsernameExists(val)),
{ message: 'Username already taken' }
)
});
// Discriminated unions
const petSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('dog'), breed: z.string() }),
z.object({ type: z.literal('cat'), color: z.string() })
]);
// Transformations
const transformSchema = z.object({
email: z.string().email().transform((val) => val.toLowerCase()),
age: z.string().pipe(z.coerce.number().min(18))
});
// API validation
async function handleUserSubmission(req: Request) {
try {
const user = await userSchema.parseAsync(req.body);
// user is fully typed as User
return { success: true, data: user };
} catch (error) {
if (error instanceof z.ZodError) {
return { success: false, errors: error.flatten() };
}
}
}
// Reusable schemas
const emailSchema = z.string().email();
const passwordSchema = z.string().min(8);
const loginSchema = z.object({
email: emailSchema,
password: passwordSchema
});
const signupSchema = loginSchema.extend({
confirmPassword: passwordSchema
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword']
});
Pros:
- Smallest bundle size
- Automatic TypeScript inference
- Modern, intuitive API
- Excellent error handling
- Built-in transformations
Cons:
- Newer ecosystem
- Fewer integrations
- Smaller community
Comparison Table
| Feature | Joi | Yup | Zod |
|---------|-----|-----|-----|
| Bundle Size | 90KB | 15KB | 8KB |
| TypeScript Support | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Type Inference | Manual | Manual | Automatic |
| Async Validation | ✅ | ✅ | ✅ |
| Error Messages | Excellent | Good | Excellent |
| Learning Curve | Steep | Moderate | Gentle |
| React Integration | Limited | Excellent | Good |
| API Validation | Excellent | Good | Excellent |
| Transformations | Limited | Good | Excellent |
| Community | Large | Large | Growing |
Decision Guide
Choose Joi if:
- Building enterprise applications
- Need maximum validation flexibility
- Team familiar with Joi ecosystem
Choose Yup if:
- Using React with Formik
- Need form validation focus
- Want smaller bundle than Joi
Choose Zod if:
- Using TypeScript
- Want smallest bundle size
- Need automatic type inference
- Building modern applications
Conclusion
All three libraries excel at schema validation. Zod leads for TypeScript projects with automatic inference and minimal bundle size. Yup remains ideal for React forms. Joi dominates enterprise scenarios requiring complex validation logic. Choose based on your project's TypeScript adoption, bundle size constraints, and integration needs.