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
});
}
});