Joi

Validation LibraryJavaScriptNode.jsSchemaServer-sideAPI

Library

Joi

Overview

Joi is developed as "The most powerful data validation library for JS" - a powerful and mature schema validation library for Node.js. With rich validation rules and flexible customization features, it can handle complex validation requirements. As a trusted presence in server-side development with years of proven track record and rich functionality, it is often chosen especially for complex API validation. Express.js integration supports robust web application development.

Details

Joi 17.13.3 is the latest version as of 2025, used by over 13,109 npm projects as the standard validation library in the Node.js ecosystem. It provides comprehensive and powerful functionality as a schema description language, enabling validation of rich types including strings, numbers, booleans, arrays, objects, and dates. Integration with Express.js middleware builds data validation pipelines at the request level, safely processing untrusted data from frontend sources.

Key Features

  • Comprehensive Validation: Wide type support from strings to complex objects
  • Rich Rule Set: Detailed constraint conditions and custom validation features
  • Express.js Integration: Easy integration as middleware with error handling
  • Flexible Schema Definition: Nested objects and conditional validation
  • Detailed Error Reporting: Specific and understandable validation error messages
  • High Customizability: Plugin functionality and custom validator support

Pros and Cons

Pros

  • Years of proven track record and stability in Node.js ecosystem
  • Rich functionality to handle complex API validation requirements
  • Excellent integration with Express.js and middleware support
  • Comprehensive official documentation and rich community support
  • Adoption track record in enterprise-level server-side applications
  • Detailed and understandable error messages with debugging support

Cons

  • Limited use in browsers (client-side)
  • Relatively large bundle size (not suitable for lightweight validation)
  • Limited TypeScript type inference support
  • Additional configuration needed for integration with modern JavaScript frameworks
  • Not suitable for functional programming style
  • Overhead in performance-focused applications

Reference Pages

Code Examples

Installation and Basic Setup

# Install Joi
npm install joi

# TypeScript type definitions (community-made)
npm install @types/joi

# Using Yarn
yarn add joi
yarn add -D @types/joi

Basic Schema Definition and Validation

const Joi = require('joi');

// Basic schema definition
const userSchema = Joi.object({
    username: Joi.string()
        .alphanum()
        .min(3)
        .max(30)
        .required(),
    
    email: Joi.string()
        .email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } })
        .required(),
    
    password: Joi.string()
        .pattern(new RegExp('^[a-zA-Z0-9]{3,30}$'))
        .required(),
    
    age: Joi.number()
        .integer()
        .min(18)
        .max(100),
    
    birth_year: Joi.number()
        .integer()
        .min(1900)
        .max(2013),
    
    access_token: [
        Joi.string(),
        Joi.number()
    ]
});

// Data validation
const userData = {
    username: 'john123',
    email: '[email protected]',
    password: 'mypassword123',
    age: 25
};

// Synchronous validation
const { error, value } = userSchema.validate(userData);

if (error) {
    console.log('Validation error:', error.details);
} else {
    console.log('Validation success:', value);
}

// Asynchronous validation
async function validateUserAsync(data) {
    try {
        const value = await userSchema.validateAsync(data);
        console.log('Validation success:', value);
        return value;
    } catch (err) {
        console.log('Validation error:', err.details);
        throw err;
    }
}

// Primitive type validation examples
const stringSchema = Joi.string().min(2).max(10);
const numberSchema = Joi.number().positive();
const booleanSchema = Joi.boolean();
const dateSchema = Joi.date().iso();

console.log(stringSchema.validate('hello')); // Success
console.log(numberSchema.validate(42)); // Success
console.log(booleanSchema.validate(true)); // Success

Advanced Validation Rules and Custom Validators

// Complex object schema
const productSchema = Joi.object({
    name: Joi.string()
        .min(1)
        .max(100)
        .required()
        .description('Product name'),
    
    price: Joi.number()
        .positive()
        .precision(2)
        .required()
        .description('Price (up to 2 decimal places)'),
    
    category: Joi.string()
        .valid('electronics', 'clothing', 'books', 'food')
        .required(),
    
    tags: Joi.array()
        .items(Joi.string().min(1))
        .min(1)
        .max(10)
        .unique(),
    
    specifications: Joi.object({
        weight: Joi.number().positive().unit('kg'),
        dimensions: Joi.object({
            width: Joi.number().positive(),
            height: Joi.number().positive(),
            depth: Joi.number().positive()
        }),
        color: Joi.string().hex(),
        material: Joi.string().optional()
    }).optional(),
    
    inStock: Joi.boolean().default(true),
    
    metadata: Joi.object().pattern(
        Joi.string(),
        [Joi.string(), Joi.number(), Joi.boolean()]
    ).optional()
});

// Conditional validation
const conditionalSchema = Joi.object({
    type: Joi.string().valid('user', 'admin').required(),
    
    username: Joi.string().when('type', {
        is: 'admin',
        then: Joi.string().min(5).required(),
        otherwise: Joi.string().min(3).required()
    }),
    
    permissions: Joi.array().when('type', {
        is: 'admin',
        then: Joi.array().items(Joi.string()).min(1).required(),
        otherwise: Joi.forbidden()
    })
});

// Custom validators
const customSchema = Joi.object({
    email: Joi.string().custom((value, helpers) => {
        // Custom email validation
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!emailRegex.test(value)) {
            return helpers.error('any.invalid');
        }
        
        // Check forbidden domains
        const forbiddenDomains = ['spam.com', 'temp.mail'];
        const domain = value.split('@')[1];
        if (forbiddenDomains.includes(domain)) {
            return helpers.message('This domain is not allowed');
        }
        
        return value.toLowerCase();
    }, 'Custom email validation'),
    
    phoneNumber: Joi.string().custom((value, helpers) => {
        // US phone number format check
        const phoneRegex = /^\+1-\d{3}-\d{3}-\d{4}$/;
        if (!phoneRegex.test(value)) {
            return helpers.error('string.pattern', { value });
        }
        return value;
    }),
    
    password: Joi.string().custom((value, helpers) => {
        // Complex password requirements
        if (value.length < 8) {
            return helpers.message('Password must be at least 8 characters');
        }
        if (!/[A-Z]/.test(value)) {
            return helpers.message('Password must contain uppercase letters');
        }
        if (!/[a-z]/.test(value)) {
            return helpers.message('Password must contain lowercase letters');
        }
        if (!/\d/.test(value)) {
            return helpers.message('Password must contain numbers');
        }
        if (!/[!@#$%^&*(),.?":{}|<>]/.test(value)) {
            return helpers.message('Password must contain special characters');
        }
        return value;
    })
});

// Detailed array validation
const arraySchema = Joi.object({
    numbers: Joi.array()
        .items(Joi.number().integer().min(1).max(100))
        .min(1)
        .max(10)
        .unique()
        .sort(),
    
    users: Joi.array().items(
        Joi.object({
            name: Joi.string().required(),
            role: Joi.string().valid('user', 'admin').default('user')
        })
    ).has(Joi.object({ role: 'admin' })) // At least one admin required
});

Framework Integration (Express, NestJS, Fastify, etc.)

// Express.js integration example
const express = require('express');
const Joi = require('joi');

const app = express();
app.use(express.json());

// Create validation middleware
const validate = (schema, property = 'body') => {
    return (req, res, next) => {
        const { error, value } = schema.validate(req[property], {
            abortEarly: false, // Get all errors
            allowUnknown: true, // Allow unknown properties
            stripUnknown: true  // Remove unknown properties
        });
        
        if (error) {
            const errors = error.details.map(detail => ({
                field: detail.path.join('.'),
                message: detail.message,
                value: detail.context.value
            }));
            
            return res.status(400).json({
                success: false,
                message: 'Validation error',
                errors: errors
            });
        }
        
        // Replace with validated data
        req[property] = value;
        next();
    };
};

// User creation API
const createUserSchema = Joi.object({
    username: Joi.string().alphanum().min(3).max(30).required(),
    email: Joi.string().email().required(),
    password: Joi.string().min(8).required(),
    profile: Joi.object({
        firstName: Joi.string().required(),
        lastName: Joi.string().required(),
        age: Joi.number().integer().min(18).max(120)
    }).optional()
});

app.post('/api/users', validate(createUserSchema), (req, res) => {
    // req.body is already validated
    console.log('Validated user data:', req.body);
    
    // User creation logic
    res.json({
        success: true,
        message: 'User created successfully',
        data: req.body
    });
});

// Query parameter validation
const getUsersQuerySchema = Joi.object({
    page: Joi.number().integer().min(1).default(1),
    limit: Joi.number().integer().min(1).max(100).default(10),
    sort: Joi.string().valid('name', 'email', 'created_at').default('created_at'),
    order: Joi.string().valid('asc', 'desc').default('desc'),
    search: Joi.string().min(2).optional()
});

app.get('/api/users', validate(getUsersQuerySchema, 'query'), (req, res) => {
    console.log('Validated query parameters:', req.query);
    
    // User retrieval logic
    res.json({
        success: true,
        data: [],
        pagination: {
            page: req.query.page,
            limit: req.query.limit,
            total: 0
        }
    });
});

// NestJS integration example (TypeScript)
/*
import { Injectable, PipeTransform, BadRequestException } from '@nestjs/common';
import * as Joi from 'joi';

@Injectable()
export class JoiValidationPipe implements PipeTransform {
    constructor(private schema: Joi.ObjectSchema) {}

    transform(value: any) {
        const { error, value: validatedValue } = this.schema.validate(value);
        if (error) {
            throw new BadRequestException('Validation error');
        }
        return validatedValue;
    }
}

// Usage example
@Post('users')
@UsePipes(new JoiValidationPipe(createUserSchema))
createUser(@Body() createUserDto: any) {
    // Use validated data
    return this.userService.create(createUserDto);
}
*/

// Fastify integration example
/*
const fastify = require('fastify')({ logger: true });

const userSchema = {
    body: {
        type: 'object',
        properties: {
            username: { type: 'string', minLength: 3, maxLength: 30 },
            email: { type: 'string', format: 'email' },
            password: { type: 'string', minLength: 8 }
        },
        required: ['username', 'email', 'password']
    }
};

fastify.post('/users', { schema: userSchema }, async (request, reply) => {
    // request.body is automatically validated
    return { success: true, data: request.body };
});
*/

Error Handling and Custom Error Messages

// Detailed error handling
const detailedSchema = Joi.object({
    name: Joi.string().min(2).max(50).required().messages({
        'string.base': 'Name must be a string',
        'string.empty': 'Please enter a name',
        'string.min': 'Name must be at least {#limit} characters',
        'string.max': 'Name must be at most {#limit} characters',
        'any.required': 'Name is required'
    }),
    
    email: Joi.string().email().required().messages({
        'string.email': 'Please enter a valid email address',
        'any.required': 'Email address is required'
    }),
    
    age: Joi.number().integer().min(0).max(150).messages({
        'number.base': 'Age must be a number',
        'number.integer': 'Age must be an integer',
        'number.min': 'Age must be at least {#limit} years',
        'number.max': 'Age must be at most {#limit} years'
    })
});

// Detailed validation error processing
function handleValidationError(error) {
    const errors = {};
    
    error.details.forEach(detail => {
        const field = detail.path.join('.');
        errors[field] = {
            message: detail.message,
            value: detail.context.value,
            type: detail.type
        };
    });
    
    return {
        success: false,
        message: 'Validation errors occurred',
        errors: errors,
        errorCount: error.details.length
    };
}

// Usage example
const testData = {
    name: 'A', // Too short
    email: 'invalid-email', // Invalid email
    age: -5 // Negative value
};

const { error } = detailedSchema.validate(testData, { abortEarly: false });
if (error) {
    const errorResponse = handleValidationError(error);
    console.log(JSON.stringify(errorResponse, null, 2));
}

// Custom error handling class
class ValidationService {
    static validate(schema, data, options = {}) {
        const defaultOptions = {
            abortEarly: false,
            allowUnknown: false,
            stripUnknown: true,
            ...options
        };
        
        const result = schema.validate(data, defaultOptions);
        
        if (result.error) {
            return {
                isValid: false,
                data: null,
                errors: this.formatErrors(result.error),
                summary: this.getErrorSummary(result.error)
            };
        }
        
        return {
            isValid: true,
            data: result.value,
            errors: null,
            summary: null
        };
    }
    
    static formatErrors(error) {
        return error.details.reduce((acc, detail) => {
            const fieldPath = detail.path.join('.');
            acc[fieldPath] = {
                message: detail.message,
                value: detail.context?.value,
                type: detail.type,
                constraint: detail.context?.limit || detail.context?.valids
            };
            return acc;
        }, {});
    }
    
    static getErrorSummary(error) {
        const totalErrors = error.details.length;
        const fieldCount = new Set(error.details.map(d => d.path[0])).size;
        const errorTypes = error.details.map(d => d.type);
        
        return {
            totalErrors,
            affectedFields: fieldCount,
            mostCommonError: this.getMostCommon(errorTypes),
            hasRequiredErrors: errorTypes.includes('any.required'),
            hasTypeErrors: errorTypes.some(type => type.includes('.base'))
        };
    }
    
    static getMostCommon(array) {
        const counts = array.reduce((acc, val) => {
            acc[val] = (acc[val] || 0) + 1;
            return acc;
        }, {});
        
        return Object.keys(counts).reduce((a, b) => 
            counts[a] > counts[b] ? a : b
        );
    }
}

// ValidationService usage example
const userValidation = ValidationService.validate(detailedSchema, testData);
console.log('Validation result:', userValidation);

Type Safety and TypeScript Integration

// TypeScript type definitions (type-safe Joi usage)
import * as Joi from 'joi';

// Helper to infer types from schema
type JoiSchemaType<T> = T extends Joi.ObjectSchema<infer U> ? U : never;

// User schema definition
const userSchemaTyped = Joi.object({
    id: Joi.number().integer().positive().required(),
    username: Joi.string().alphanum().min(3).max(30).required(),
    email: Joi.string().email().required(),
    isActive: Joi.boolean().default(true),
    profile: Joi.object({
        firstName: Joi.string().required(),
        lastName: Joi.string().required(),
        age: Joi.number().integer().min(18).optional(),
        avatar: Joi.string().uri().optional()
    }).optional(),
    roles: Joi.array().items(Joi.string().valid('admin', 'user', 'moderator')).default(['user']),
    metadata: Joi.object().pattern(Joi.string(), Joi.any()).optional(),
    createdAt: Joi.date().default(() => new Date()),
    lastLogin: Joi.date().optional()
});

// Type definition
interface User {
    id: number;
    username: string;
    email: string;
    isActive: boolean;
    profile?: {
        firstName: string;
        lastName: string;
        age?: number;
        avatar?: string;
    };
    roles: Array<'admin' | 'user' | 'moderator'>;
    metadata?: { [key: string]: any };
    createdAt: Date;
    lastLogin?: Date;
}

// Type-safe validation function
function validateUser(data: unknown): { isValid: true; data: User } | { isValid: false; error: Joi.ValidationError } {
    const result = userSchemaTyped.validate(data);
    
    if (result.error) {
        return { isValid: false, error: result.error };
    }
    
    return { isValid: true, data: result.value as User };
}

// API function usage example
async function createUser(userData: unknown): Promise<User> {
    const validation = validateUser(userData);
    
    if (!validation.isValid) {
        throw new Error(`Validation error: ${validation.error.message}`);
    }
    
    // validation.data is type-safe as User
    const user = validation.data;
    console.log(`User created: ${user.username} (${user.email})`);
    
    // Database save logic
    return user;
}

// Generic validation function
class TypedValidator<T> {
    constructor(private schema: Joi.ObjectSchema<T>) {}
    
    validate(data: unknown): { success: true; data: T } | { success: false; errors: string[] } {
        const result = this.schema.validate(data, { abortEarly: false });
        
        if (result.error) {
            return {
                success: false,
                errors: result.error.details.map(detail => detail.message)
            };
        }
        
        return {
            success: true,
            data: result.value as T
        };
    }
    
    async validateAsync(data: unknown): Promise<T> {
        const result = await this.schema.validateAsync(data);
        return result as T;
    }
}

// Type-safe validator usage
const userValidator = new TypedValidator<User>(userSchemaTyped);

const testUserData = {
    id: 1,
    username: 'johndoe',
    email: '[email protected]',
    profile: {
        firstName: 'John',
        lastName: 'Doe',
        age: 30
    }
};

const validationResult = userValidator.validate(testUserData);
if (validationResult.success) {
    // validationResult.data is type-safe as User
    console.log(`Validated user: ${validationResult.data.username}`);
} else {
    console.log('Errors:', validationResult.errors);
}

// Type-safe validation for complex nested objects
interface APIResponse<T> {
    success: boolean;
    data: T;
    message: string;
    timestamp: Date;
}

function createAPIResponseSchema<T>(dataSchema: Joi.Schema<T>) {
    return Joi.object({
        success: Joi.boolean().required(),
        data: dataSchema.required(),
        message: Joi.string().required(),
        timestamp: Joi.date().required()
    });
}

const userAPIResponseSchema = createAPIResponseSchema(userSchemaTyped);
type UserAPIResponse = APIResponse<User>;

// Practical example: Type-safe usage in Express.js
/*
import { Request, Response, NextFunction } from 'express';

interface TypedRequest<T> extends Request {
    validatedBody: T;
}

function validateBody<T>(schema: Joi.ObjectSchema<T>) {
    return (req: Request, res: Response, next: NextFunction) => {
        const result = schema.validate(req.body);
        
        if (result.error) {
            return res.status(400).json({
                success: false,
                message: 'Validation error',
                errors: result.error.details.map(d => d.message)
            });
        }
        
        (req as TypedRequest<T>).validatedBody = result.value as T;
        next();
    };
}

// Usage example
app.post('/users', validateBody(userSchemaTyped), (req: TypedRequest<User>, res: Response) => {
    // req.validatedBody is type-safe as User
    const user = req.validatedBody;
    console.log(`New user: ${user.username}`);
    res.json({ success: true, data: user });
});
*/