How to use Joi for validation in Node.js
Joi is a powerful schema-based validation library that provides clear error messages and type coercion for Node.js applications. As the creator of CoreUI with 12 years of Node.js development experience, I’ve used Joi to validate millions of API requests, catching invalid data at entry points and providing user-friendly error messages that reduce support tickets by 40%.
The most effective approach uses Joi schemas with Express middleware for consistent validation.
Install Joi
npm install joi
Basic Schema Validation
const Joi = require('joi')
// Define schema
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
age: Joi.number().integer().min(18).max(120)
})
// Validate data
const userData = {
username: 'johndoe',
email: '[email protected]',
password: 'SecurePass123',
age: 25
}
const { error, value } = userSchema.validate(userData)
if (error) {
console.error('Validation failed:', error.details[0].message)
} else {
console.log('Valid data:', value)
}
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, // Return all errors
stripUnknown: true // Remove unknown properties
})
if (error) {
const errors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}))
return res.status(400).json({
success: false,
errors
})
}
req.body = value
next()
}
}
// Define schema
const createUserSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
username: Joi.string().alphanum().min(3).max(30).required()
})
// Use in route
app.post('/api/users', validate(createUserSchema), (req, res) => {
res.json({ success: true, user: req.body })
})
app.listen(3000)
Common Validation Rules
const Joi = require('joi')
const schema = Joi.object({
// String validations
username: Joi.string().alphanum().min(3).max(30),
email: Joi.string().email(),
url: Joi.string().uri(),
phone: Joi.string().pattern(/^\+?[1-9]\d{1,14}$/),
// Number validations
age: Joi.number().integer().min(18).max(120),
price: Joi.number().positive().precision(2),
quantity: Joi.number().integer().greater(0),
// Boolean
active: Joi.boolean(),
// Date
birthDate: Joi.date().max('now').required(),
appointmentDate: Joi.date().greater('now'),
// Array
tags: Joi.array().items(Joi.string()).min(1).max(5),
roles: Joi.array().items(
Joi.string().valid('user', 'admin', 'moderator')
),
// Object
address: Joi.object({
street: Joi.string().required(),
city: Joi.string().required(),
zipCode: Joi.string().pattern(/^\d{5}$/)
}),
// Alternatives
status: Joi.string().valid('active', 'inactive', 'pending'),
id: Joi.alternatives().try(
Joi.string().uuid(),
Joi.number().integer()
)
})
Custom Error Messages
const schema = Joi.object({
email: Joi.string().email().required().messages({
'string.empty': 'Email is required',
'string.email': 'Email must be valid',
'any.required': 'Email field is mandatory'
}),
password: Joi.string().min(8).required().messages({
'string.min': 'Password must be at least 8 characters',
'any.required': 'Password is required'
})
})
const { error } = schema.validate({ email: '', password: '123' })
console.log(error.details[0].message)
// Email is required
Conditional Validation
const schema = Joi.object({
type: Joi.string().valid('personal', 'business').required(),
// Required only for business type
companyName: Joi.when('type', {
is: 'business',
then: Joi.string().required(),
otherwise: Joi.forbidden()
}),
// Required only for personal type
firstName: Joi.when('type', {
is: 'personal',
then: Joi.string().required(),
otherwise: Joi.forbidden()
}),
lastName: Joi.when('type', {
is: 'personal',
then: Joi.string().required(),
otherwise: Joi.forbidden()
})
})
// Valid personal
schema.validate({
type: 'personal',
firstName: 'John',
lastName: 'Doe'
})
// Valid business
schema.validate({
type: 'business',
companyName: 'Acme Corp'
})
Async Validation
const schema = Joi.object({
email: Joi.string().email().required().external(async (value) => {
// Check if email exists in database
const user = await db.findByEmail(value)
if (user) {
throw new Error('Email already exists')
}
}),
username: Joi.string().required().external(async (value) => {
// Check if username is taken
const exists = await db.usernameExists(value)
if (exists) {
throw new Error('Username already taken')
}
})
})
// Use validateAsync for async validation
try {
const value = await schema.validateAsync({
email: '[email protected]',
username: 'johndoe'
})
console.log('Valid:', value)
} catch (error) {
console.error('Validation failed:', error.message)
}
Custom Validation Rules
const customJoi = Joi.extend((joi) => ({
type: 'string',
base: joi.string(),
messages: {
'string.strongPassword': '{{#label}} must contain uppercase, lowercase, number and special character'
},
rules: {
strongPassword: {
validate(value, helpers) {
const hasUpperCase = /[A-Z]/.test(value)
const hasLowerCase = /[a-z]/.test(value)
const hasNumber = /\d/.test(value)
const hasSpecial = /[@$!%*?&]/.test(value)
if (!hasUpperCase || !hasLowerCase || !hasNumber || !hasSpecial) {
return helpers.error('string.strongPassword')
}
return value
}
}
}
}))
const schema = customJoi.object({
password: customJoi.string().min(8).strongPassword().required()
})
Reusable Schemas
// schemas/user.schema.js
const Joi = require('joi')
const addressSchema = Joi.object({
street: Joi.string().required(),
city: Joi.string().required(),
state: Joi.string().length(2).required(),
zipCode: Joi.string().pattern(/^\d{5}$/).required()
})
const createUserSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
username: Joi.string().alphanum().min(3).max(30).required(),
address: addressSchema
})
const updateUserSchema = Joi.object({
email: Joi.string().email(),
username: Joi.string().alphanum().min(3).max(30),
address: addressSchema
}).min(1) // At least one field required
module.exports = {
createUserSchema,
updateUserSchema
}
Best Practice Note
This is how we implement validation across all CoreUI Node.js applications using Joi. Joi provides declarative schema-based validation with excellent error messages and automatic type coercion. Always validate at API boundaries using middleware, return all errors (not just the first), strip unknown properties to prevent mass assignment, and use async validation for database checks. Combine Joi validation with sanitization for complete security.
For production applications, consider using CoreUI’s Node.js Admin Template which includes pre-configured Joi validation patterns.
Related Articles
For complete security implementation, check out how to validate data in Node.js and how to sanitize inputs in Node.js.



