Joi Complete Guide
이 글의 핵심
Joi is the most powerful schema description language and data validator for JavaScript. It provides rich validation capabilities with excellent error messages.
Introduction
Joi is the most feature-rich schema validation library for JavaScript. It’s designed for Node.js backends where complex validation logic and detailed error messages are crucial.
Why Joi?
Manual validation:
function validateUser(data) {
if (!data.email || typeof data.email !== 'string') {
throw new Error('Invalid email');
}
if (!data.age || typeof data.age !== 'number' || data.age < 18) {
throw new Error('Invalid age');
}
// ... dozens more checks
}
With Joi:
const Joi = require('joi');
const schema = Joi.object({
email: Joi.string().email().required(),
age: Joi.number().integer().min(18).required(),
});
const { error, value } = schema.validate(data);
1. Installation
npm install joi
2. Basic Validation
const Joi = require('joi');
// Define schema
const schema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(18).max(120),
password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')),
});
// Validate data
const data = {
username: 'john_doe',
email: 'john@example.com',
age: 25,
password: 'secret123',
};
const { error, value } = schema.validate(data);
if (error) {
console.error(error.details);
} else {
console.log('Valid:', value);
}
3. String Validation
Joi.string()
.required()
.min(2)
.max(100)
.email()
.uri()
.alphanum()
.lowercase()
.uppercase()
.trim()
.pattern(/^[a-zA-Z]+$/)
.length(10)
.creditCard()
.domain()
.ip()
.dataUri()
.hex()
.base64()
.guid() // UUID
.isoDate();
// Examples
const emailSchema = Joi.string().email().required();
const urlSchema = Joi.string().uri().required();
const uuidSchema = Joi.string().guid({ version: 'uuidv4' });
const phoneSchema = Joi.string().pattern(/^\+?[1-9]\d{1,14}$/);
4. Number Validation
Joi.number()
.required()
.integer()
.min(0)
.max(100)
.positive()
.negative()
.greater(10)
.less(100)
.multiple(5)
.precision(2)
.port();
// Examples
const ageSchema = Joi.number().integer().min(18).max(120).required();
const priceSchema = Joi.number().precision(2).positive().required();
const portSchema = Joi.number().port();
5. Object Validation
const userSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
address: Joi.object({
street: Joi.string().required(),
city: Joi.string().required(),
zipCode: Joi.string().pattern(/^\d{5}$/),
country: Joi.string().required(),
}),
settings: Joi.object({
notifications: Joi.boolean().default(true),
theme: Joi.string().valid('light', 'dark').default('light'),
}),
});
// Validate
const result = userSchema.validate({
name: 'Alice',
email: 'alice@example.com',
address: {
street: '123 Main St',
city: 'New York',
zipCode: '10001',
country: 'USA',
},
});
6. Array Validation
// Array of strings
const tagsSchema = Joi.array()
.items(Joi.string())
.min(1)
.max(5)
.unique();
// Array of objects
const usersSchema = Joi.array().items(
Joi.object({
id: Joi.number().required(),
name: Joi.string().required(),
email: Joi.string().email().required(),
})
);
// Ordered items (tuple)
const coordsSchema = Joi.array().ordered(
Joi.number().required(), // latitude
Joi.number().required() // longitude
);
// Mixed types
const mixedSchema = Joi.array().items(
Joi.string(),
Joi.number(),
Joi.boolean()
);
await tagsSchema.validateAsync(['react', 'typescript', 'nodejs']);
7. Conditional Validation
const schema = Joi.object({
accountType: Joi.string().valid('personal', 'business').required(),
// Required only for business accounts
companyName: Joi.string().when('accountType', {
is: 'business',
then: Joi.string().required(),
otherwise: Joi.forbidden(),
}),
// Multiple conditions
taxId: Joi.string().when(Joi.object({
accountType: Joi.string().valid('business'),
country: Joi.string().valid('US'),
}).unknown(), {
then: Joi.string().required(),
otherwise: Joi.optional(),
}),
country: Joi.string().required(),
});
8. Custom Validation
// Custom validation function
const passwordSchema = Joi.string().custom((value, helpers) => {
if (!/[A-Z]/.test(value)) {
return helpers.error('any.invalid');
}
if (!/[a-z]/.test(value)) {
return helpers.error('any.invalid');
}
if (!/[0-9]/.test(value)) {
return helpers.error('any.invalid');
}
return value;
}, 'strong password validation');
// With custom error message
const emailSchema = Joi.string()
.email()
.custom((value, helpers) => {
// Block disposable email domains
const disposableDomains = ['tempmail.com', '10minutemail.com'];
const domain = value.split('@')[1];
if (disposableDomains.includes(domain)) {
return helpers.error('string.disposableEmail');
}
return value;
})
.messages({
'string.disposableEmail': 'Disposable email addresses are not allowed',
});
9. Async Validation
const usernameSchema = Joi.string().external(async (value) => {
// Check if username exists in database
const exists = await db.users.findOne({ username: value });
if (exists) {
throw new Error('Username already taken');
}
return value;
});
// Usage
const result = await usernameSchema.validateAsync('john_doe');
10. Express Middleware
const express = require('express');
const Joi = require('joi');
const app = express();
app.use(express.json());
// Validation middleware
function validate(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true,
});
if (error) {
const errors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
}));
return res.status(400).json({ errors });
}
req.body = value;
next();
};
}
// Define schemas
const createUserSchema = Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
age: Joi.number().integer().min(18).required(),
});
// Use in route
app.post('/users', validate(createUserSchema), (req, res) => {
// req.body is validated and sanitized
res.json({ message: 'User created', user: req.body });
});
11. Advanced Middleware
// Validate multiple sources
function validateRequest(schemas) {
return (req, res, next) => {
const errors = [];
// Validate body
if (schemas.body) {
const { error } = schemas.body.validate(req.body);
if (error) errors.push(...error.details);
}
// Validate query
if (schemas.query) {
const { error } = schemas.query.validate(req.query);
if (error) errors.push(...error.details);
}
// Validate params
if (schemas.params) {
const { error } = schemas.params.validate(req.params);
if (error) errors.push(...error.details);
}
if (errors.length > 0) {
return res.status(400).json({ errors });
}
next();
};
}
// Usage
app.get('/users/:id', validateRequest({
params: Joi.object({
id: Joi.number().integer().required(),
}),
query: Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(10),
}),
}), (req, res) => {
res.json({ userId: req.params.id, page: req.query.page });
});
12. Error Handling
const schema = Joi.object({
email: Joi.string().email().required(),
age: Joi.number().min(18).required(),
});
const { error } = schema.validate({ email: 'invalid', age: 15 }, {
abortEarly: false, // Get all errors
});
if (error) {
console.log(error.details);
// [
// {
// message: '"email" must be a valid email',
// path: ['email'],
// type: 'string.email',
// context: { value: 'invalid', label: 'email', key: 'email' }
// },
// {
// message: '"age" must be greater than or equal to 18',
// path: ['age'],
// type: 'number.min',
// context: { limit: 18, value: 15, label: 'age', key: 'age' }
// }
// ]
}
Custom Error Messages
const schema = Joi.object({
email: Joi.string().email().required().messages({
'string.email': 'Please provide a valid email address',
'any.required': 'Email is required',
}),
password: Joi.string().min(8).required().messages({
'string.min': 'Password must be at least 8 characters long',
'any.required': 'Password is required',
}),
});
13. Validation Options
const options = {
abortEarly: false, // Return all errors
allowUnknown: true, // Allow unknown keys
stripUnknown: true, // Remove unknown keys
convert: true, // Type conversion
presence: 'required', // All keys required by default
noDefaults: false, // Apply default values
escapeHtml: true, // Escape HTML
};
const { error, value } = schema.validate(data, options);
14. Reusable Schemas
// Base schemas
const idSchema = Joi.number().integer().positive();
const emailSchema = Joi.string().email().required();
const timestampSchema = Joi.date().iso();
// Compose larger schemas
const userSchema = Joi.object({
id: idSchema,
email: emailSchema,
createdAt: timestampSchema,
updatedAt: timestampSchema,
});
// Extend schemas
const adminSchema = userSchema.keys({
role: Joi.string().valid('admin', 'superadmin').required(),
permissions: Joi.array().items(Joi.string()).required(),
});
15. Real-World Example: Blog API
const Joi = require('joi');
// Post schema
const postSchema = Joi.object({
title: Joi.string().min(5).max(200).required(),
content: Joi.string().min(10).required(),
excerpt: Joi.string().max(300),
tags: Joi.array().items(Joi.string()).min(1).max(5).unique(),
published: Joi.boolean().default(false),
publishedAt: Joi.date().when('published', {
is: true,
then: Joi.required(),
otherwise: Joi.forbidden(),
}),
author: Joi.object({
id: Joi.number().required(),
name: Joi.string().required(),
}).required(),
});
// Comment schema
const commentSchema = Joi.object({
postId: Joi.number().required(),
content: Joi.string().min(1).max(1000).required(),
author: Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
}).required(),
});
// Query schema
const postQuerySchema = Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(10),
sort: Joi.string().valid('createdAt', 'title', 'views').default('createdAt'),
order: Joi.string().valid('asc', 'desc').default('desc'),
tags: Joi.alternatives().try(
Joi.string(),
Joi.array().items(Joi.string())
),
published: Joi.boolean(),
});
// Routes
app.post('/posts', validate(postSchema), createPost);
app.get('/posts', validate(postQuerySchema, 'query'), getPosts);
app.post('/posts/:id/comments', validate(commentSchema), createComment);
16. Best Practices
1. Use Strict Mode
const schema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
}).strict(); // Reject unknown keys
2. Sanitize Input
const schema = Joi.object({
name: Joi.string().trim().lowercase(),
email: Joi.string().email().lowercase().trim(),
});
3. Provide Clear Error Messages
const schema = Joi.object({
password: Joi.string()
.min(8)
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.messages({
'string.min': 'Password must be at least 8 characters',
'string.pattern.base': 'Password must include uppercase, lowercase, and number',
}),
});
Summary
Joi provides enterprise-grade validation:
- Rich validation rules for any data type
- Detailed error messages for debugging
- Conditional logic for complex scenarios
- Async validation for database checks
- Express integration via middleware
Key Takeaways:
- Use for backend API validation
- Detailed error reporting out of the box
- Conditional validation with when()
- Async validation with external()
- Express middleware for clean routes
Next Steps:
- Compare Yup
- Try Zod
- Build Express API
Resources:
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Joi is the most powerful schema description language and data validator for JavaScript. It provides rich validation capabilities with excellent error messages.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- Yup Complete Guide | Schema Validation for JavaScript
- Mongoose Complete Guide | MongoDB ODM for Node.js
- Zod Complete Guide: TypeScript-First Schema Validation Library
Keywords Covered in This Article (Related Search Terms)
This article covers Joi, Validation, JavaScript, Node.js, Backend, Web-API.