Understanding Model Validations in Sequelize: From Basics to Advanced

Learn how to ensure data integrity through comprehensive model validations

Understanding Model Validations

Think of model validations as quality control inspectors in a factory. Just as inspectors check products before they leave the factory floor, model validations examine data before it enters your database. These inspectors can be as thorough and specific as you need them to be, checking everything from basic requirements to complex business rules.

Consider an online bookstore. When a new book is being added to the system, we need to ensure several things:

The price must be positive and reasonable

The ISBN must be valid and unique

The title and author cannot be empty

The publication date must be valid and not in the future

Let's see how we can implement these validations using Sequelize:


class Book extends Model {}

Book.init({
    title: {
        type: DataTypes.STRING,
        allowNull: false,  // Database constraint
        validate: {       // Model validation
            notEmpty: {
                msg: 'Book title cannot be empty'
            },
            len: {
                args: [1, 255],
                msg: 'Title must be between 1 and 255 characters'
            }
        }
    },
    price: {
        type: DataTypes.DECIMAL(10, 2),
        allowNull: false,
        validate: {
            isDecimal: {
                msg: 'Price must be a valid decimal number'
            },
            min: {
                args: [0.01],
                msg: 'Price must be greater than zero'
            },
            max: {
                args: [9999.99],
                msg: 'Price seems unreasonably high'
            },
            isPriceReasonable(value) {
                // Custom validation: Flag suspiciously low or high prices
                if (value < 1.00) {
                    throw new Error('Price seems suspiciously low');
                }
                if (value > 1000.00) {
                    throw new Error('Please double-check this price');
                }
            }
        }
    },
    isbn: {
        type: DataTypes.STRING(13),
        unique: true,     // Database constraint
        validate: {
            isValidISBN(value) {
                // Custom ISBN-13 validation
                if (!/^[0-9]{13}$/.test(value)) {
                    throw new Error('ISBN must be exactly 13 digits');
                }
                // Here you could add actual ISBN checksum validation
            }
        }
    },
    publishDate: {
        type: DataTypes.DATE,
        validate: {
            isDate: {
                msg: 'Publication date must be a valid date'
            },
            notInFuture(value) {
                if (new Date(value) > new Date()) {
                    throw new Error('Publication date cannot be in the future');
                }
            }
        }
    }
}, {
    sequelize,
    modelName: 'Book'
});
                

Types of Validations

Sequelize offers multiple layers of validation, each serving a specific purpose. Let's explore them using our bookstore example:

Built-in Validators


// Example of built-in validators for an Author model
class Author extends Model {}

Author.init({
    email: {
        type: DataTypes.STRING,
        validate: {
            isEmail: {
                msg: 'Please provide a valid email address'
            }
        }
    },
    website: {
        type: DataTypes.STRING,
        validate: {
            isURL: {
                msg: 'Please provide a valid website URL',
                protocols: ['http', 'https'],
                require_protocol: true
            }
        }
    },
    phoneNumber: {
        type: DataTypes.STRING,
        validate: {
            is: {
                args: /^\+?[1-9]\d{1,14}$/,
                msg: 'Please provide a valid phone number'
            }
        }
    }
});
                

Custom Validators


// Example of custom validators for a BookReview model
class BookReview extends Model {}

BookReview.init({
    rating: {
        type: DataTypes.INTEGER,
        validate: {
            isWithinScale(value) {
                if (value < 1 || value > 5) {
                    throw new Error('Rating must be between 1 and 5 stars');
                }
            }
        }
    },
    review: {
        type: DataTypes.TEXT,
        validate: {
            isProfessional(value) {
                const forbiddenWords = ['terrible', 'horrible', 'worst'];
                if (forbiddenWords.some(word => 
                    value.toLowerCase().includes(word))) {
                    throw new Error('Please use professional language in reviews');
                }
            },
            hasSubstance(value) {
                if (value.split(' ').length < 5) {
                    throw new Error('Review must contain at least 5 words');
                }
            }
        }
    }
});
                

Cross-Field Validations

Sometimes we need to validate data based on multiple fields. Here's how to implement these more complex validations:


class Order extends Model {}

Order.init({
    startDate: {
        type: DataTypes.DATE
    },
    endDate: {
        type: DataTypes.DATE
    },
    discountCode: {
        type: DataTypes.STRING
    },
    totalAmount: {
        type: DataTypes.DECIMAL(10, 2)
    }
}, {
    sequelize,
    validate: {
        dateRange() {
            if (this.endDate <= this.startDate) {
                throw new Error('End date must be after start date');
            }
        },
        discountValidation() {
            if (this.discountCode && this.totalAmount <= 0) {
                throw new Error('Cannot apply discount to zero-amount order');
            }
        }
    }
});
                

Advanced Validation Patterns

Let's explore some advanced validation scenarios and how to handle them effectively:

Conditional Validation


class Product extends Model {}

Product.init({
    name: {
        type: DataTypes.STRING,
        validate: {
            notEmpty: true
        }
    },
    digitalDownloadUrl: {
        type: DataTypes.STRING,
        validate: {
            async isValidDownloadUrl(value) {
                if (this.type === 'digital' && !value) {
                    throw new Error('Digital products must have a download URL');
                }
                if (value) {
                    // Check if URL is accessible
                    try {
                        const response = await fetch(value, { method: 'HEAD' });
                        if (!response.ok) {
                            throw new Error('Download URL is not accessible');
                        }
                    } catch (error) {
                        throw new Error('Failed to validate download URL');
                    }
                }
            }
        }
    },
    type: {
        type: DataTypes.ENUM('physical', 'digital')
    },
    weight: {
        type: DataTypes.FLOAT,
        validate: {
            isValidWeight(value) {
                if (this.type === 'physical' && !value) {
                    throw new Error('Physical products must have a weight');
                }
                if (this.type === 'digital' && value) {
                    throw new Error('Digital products should not have a weight');
                }
            }
        }
    }
});
                

Custom Validation with External API


class Address extends Model {}

Address.init({
    street: DataTypes.STRING,
    city: DataTypes.STRING,
    state: DataTypes.STRING,
    zipCode: {
        type: DataTypes.STRING,
        validate: {
            async isValidAddress() {
                const address = {
                    street: this.street,
                    city: this.city,
                    state: this.state,
                    zipCode: this.zipCode
                };

                try {
                    // This is a hypothetical address verification service
                    const isValid = await AddressVerificationService.verify(address);
                    if (!isValid) {
                        throw new Error('Invalid address');
                    }
                } catch (error) {
                    throw new Error('Failed to verify address');
                }
            }
        }
    }
});
                

Best Practices and Error Handling

Proper error handling is crucial when working with validations. Here's how to implement robust error handling:


// Helper function for handling validation errors
async function createWithValidation(model, data) {
    try {
        const instance = await model.create(data);
        return {
            success: true,
            data: instance
        };
    } catch (error) {
        if (error instanceof ValidationError) {
            return {
                success: false,
                errors: error.errors.map(err => ({
                    field: err.path,
                    message: err.message,
                    type: err.validatorKey
                }))
            };
        }
        
        throw error; // Re-throw unexpected errors
    }
}

// Example usage
app.post('/books', async (req, res) => {
    const result = await createWithValidation(Book, req.body);
    
    if (result.success) {
        res.status(201).json(result.data);
    } else {
        res.status(400).json({
            message: 'Validation failed',
            errors: result.errors
        });
    }
});