Concept
Object Data Modeling (ODM) with Schemas
Although MongoDB is schemaless by design, production applications require structured schemas to prevent garbage data from entering collections.
Mongoose defines models based on a schema configuration, casting values to specified types automatically:
import mongoose from 'mongoose';
const UserSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: {
type: String,
required: true,
match: /.+\@.+\..+/ // Validation regex
},
role: { type: String, enum: ['user', 'admin'], default: 'user' },
createdAt: { type: Date, default: Date.now }
});
const User = mongoose.model('User', UserSchema);Mongoose Middleware (Hooks)
Mongoose schemas support middleware (pre and post hooks) to execute logic during document lifecycles (such as hashing passwords before saving):
UserSchema.pre('save', async function(next) {
if (this.isModified('password')) {
this.password = await hashPassword(this.password);
}
next();
});Populating References
Mongoose simplifies document relationships using ref pointers and .populate(), which simulates SQL joins at the ODM layer:
const PostSchema = new mongoose.Schema({
title: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
});
// Queries post and populates author data
const posts = await Post.find().populate('author', 'username email');Common Mistakes
1. Assuming .populate() is a real SQL database join
Behind the scenes, Mongoose's .populate() runs a second query (using $in array filters) to fetch user records. It is not an atomic database join. Heavy populate chains cause severe performance bottlenecks on large collections.
2. Forgetting that validation does not run on direct update queries
By default, Mongoose validation rules (like min, max, or match) only execute during .save() or .create(). Direct operations like Model.updateOne() bypass validation checks unless explicitly enabled:
// Validation enabled for update query
await User.updateOne({ _id }, { $set: { email: 'bad-email' } }, { runValidators: true });Best Practices
- Enable runValidators: Always configure
{ runValidators: true }inside update queries to enforce schema integrity. - Index Unique Fields: Schema validation configurations like
unique: trueare helper flags. Always build a matching unique index in MongoDB directly to prevent duplicate writes under concurrency. - Implement Virtuals: Use Mongoose virtual attributes (like
fullNamecomputed fromfirstNameandlastName) for read-only fields that do not need storage.
