Table Of Contents
Quick Fix: Basic Schema with Validation
const mongoose = require('mongoose');
// Basic schema with validation
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
minlength: [2, 'Name must be at least 2 characters'],
maxlength: [50, 'Name cannot exceed 50 characters'],
trim: true
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Invalid email format']
},
age: {
type: Number,
min: [0, 'Age must be positive'],
max: [120, 'Age cannot exceed 120']
}
}, {
timestamps: true // Adds createdAt and updatedAt
});
const User = mongoose.model('User', userSchema);
The Problem: Advanced Schema Design
// Complex schema with custom validation
const userSchema = new mongoose.Schema({
// String validations
username: {
type: String,
required: [true, 'Username is required'],
unique: true,
minlength: 3,
maxlength: 20,
match: [/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'],
index: true
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
validate: {
validator: function(email) {
return /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(email);
},
message: 'Please provide a valid email address'
}
},
// Password with custom validation
password: {
type: String,
required: [true, 'Password is required'],
minlength: [8, 'Password must be at least 8 characters'],
validate: {
validator: function(password) {
// At least one uppercase, one lowercase, one number, one special char
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/.test(password);
},
message: 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character'
},
select: false // Don't include in queries by default
},
// Nested objects
profile: {
firstName: { type: String, trim: true },
lastName: { type: String, trim: true },
bio: {
type: String,
maxlength: [500, 'Bio cannot exceed 500 characters']
},
avatar: {
url: String,
filename: String
},
socialLinks: {
twitter: {
type: String,
validate: {
validator: function(v) {
return !v || /^@[A-Za-z0-9_]+$/.test(v);
},
message: 'Twitter handle must start with @ and contain only letters, numbers, and underscores'
}
},
linkedin: String,
website: {
type: String,
validate: {
validator: function(v) {
return !v || /^https?:\/\/.+/.test(v);
},
message: 'Website must be a valid URL'
}
}
}
},
// Arrays with validation
skills: [{
type: String,
trim: true,
maxlength: 30
}],
// Array with length validation
tags: {
type: [String],
validate: {
validator: function(tags) {
return tags.length <= 10;
},
message: 'Cannot have more than 10 tags'
}
},
// Enum validation
role: {
type: String,
enum: {
values: ['user', 'admin', 'moderator'],
message: 'Role must be either user, admin, or moderator'
},
default: 'user'
},
// Date validations
dateOfBirth: {
type: Date,
validate: {
validator: function(date) {
return date < new Date();
},
message: 'Date of birth must be in the past'
}
},
// Custom field with async validation
phoneNumber: {
type: String,
validate: {
validator: async function(phone) {
if (!phone) return true; // Optional field
// Check if phone number already exists
const existingUser = await mongoose.model('User').findOne({
phoneNumber: phone,
_id: { $ne: this._id } // Exclude current document
});
return !existingUser;
},
message: 'Phone number already exists'
}
},
// Boolean with default
isActive: {
type: Boolean,
default: true
},
// Reference to another model
company: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Company'
},
// Array of references
projects: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Project'
}]
}, {
timestamps: true,
// Schema options
toJSON: {
transform: function(doc, ret) {
delete ret.password;
delete ret.__v;
return ret;
}
},
toObject: {
transform: function(doc, ret) {
delete ret.password;
return ret;
}
}
});
// Compound indexes
userSchema.index({ email: 1, isActive: 1 });
userSchema.index({ 'profile.firstName': 1, 'profile.lastName': 1 });
// Virtual properties
userSchema.virtual('fullName').get(function() {
return `${this.profile.firstName} ${this.profile.lastName}`.trim();
});
userSchema.virtual('age').get(function() {
if (!this.dateOfBirth) return null;
return Math.floor((Date.now() - this.dateOfBirth) / (365.25 * 24 * 60 * 60 * 1000));
});
// Custom validation methods
userSchema.methods.isAdult = function() {
return this.age >= 18;
};
// Static methods for validation
userSchema.statics.validateEmail = function(email) {
return /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(email);
};
// Custom schema type for enhanced validation
function EmailType(key, options) {
mongoose.SchemaType.call(this, key, options, 'EmailType');
}
EmailType.prototype = Object.create(mongoose.SchemaType.prototype);
EmailType.prototype.cast = function(val) {
if (typeof val !== 'string') {
throw new Error('Email must be a string');
}
const email = val.toLowerCase().trim();
if (!this.constructor.validateEmail(email)) {
throw new Error('Invalid email format');
}
return email;
};
EmailType.validateEmail = function(email) {
return /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(email);
};
mongoose.Schema.Types.EmailType = EmailType;
// Schema with conditional validation
const productSchema = new mongoose.Schema({
name: { type: String, required: true },
type: {
type: String,
enum: ['physical', 'digital'],
required: true
},
// Conditional validation based on type
weight: {
type: Number,
required: function() {
return this.type === 'physical';
},
min: [0, 'Weight must be positive']
},
downloadUrl: {
type: String,
required: function() {
return this.type === 'digital';
},
validate: {
validator: function(url) {
if (this.type !== 'digital') return true;
return /^https?:\/\/.+/.test(url);
},
message: 'Download URL must be valid for digital products'
}
},
price: {
type: Number,
required: true,
min: [0, 'Price must be positive'],
validate: {
validator: function(price) {
// Custom business rule
if (this.type === 'digital' && price < 1) {
return false;
}
return true;
},
message: 'Digital products must cost at least $1'
}
}
});
// Schema with pre-validation hook
userSchema.pre('validate', function(next) {
// Ensure email domain is allowed
const allowedDomains = ['gmail.com', 'yahoo.com', 'company.com'];
if (this.email) {
const domain = this.email.split('@')[1];
if (!allowedDomains.includes(domain)) {
this.invalidate('email', 'Email domain not allowed');
}
}
next();
});
// Schema with custom error handling
userSchema.post('save', function(error, doc, next) {
if (error.name === 'MongoServerError' && error.code === 11000) {
const field = Object.keys(error.keyValue)[0];
next(new Error(`${field} already exists`));
} else {
next(error);
}
});
const User = mongoose.model('User', userSchema);
Mongoose schemas solve "data validation", "document structure", and "business rule enforcement" issues. Use built-in validators for common cases, custom validators for business logic. Always validate on both client and server. Alternative: Joi for validation, TypeScript for type safety, JSON Schema validation.
Share this article
Add Comment
No comments yet. Be the first to comment!