Difficulty: Intermediate
MongoDB is a document-oriented NoSQL database that stores data as flexible JSON-like documents (BSON). Unlike relational databases with rigid table schemas, MongoDB collections can hold documents with varying structures. This flexibility makes it popular for rapid prototyping, content management systems, and applications where data structures evolve frequently. However, this flexibility can also lead to inconsistent data if not managed carefully.
Mongoose is the most popular ODM (Object Document Mapper) for MongoDB in Node.js. It provides a schema-based solution that adds structure, validation, and convenience methods on top of MongoDB's native driver. With Mongoose, you define a schema that specifies the fields, types, defaults, and validation rules for your documents. The schema is then compiled into a model - a constructor function that creates and queries documents in a specific collection.
Connecting to MongoDB with Mongoose is straightforward. You call `mongoose.connect()` with a connection string, and Mongoose handles connection pooling, reconnection, and buffering commands until the connection is ready. In production, you should handle connection errors, use environment variables for the connection string, and configure options like connection timeouts and replica sets.
CRUD operations in Mongoose are intuitive. You create documents with `Model.create()` or `new Model().save()`, read with `Model.find()`, `Model.findOne()`, or `Model.findById()`, update with `Model.findByIdAndUpdate()` or `Model.updateMany()`, and delete with `Model.findByIdAndDelete()` or `Model.deleteMany()`. Mongoose also supports powerful query builders with chaining: `User.find({ age: { $gte: 18 } }).sort('-createdAt').limit(10).select('name email')`.
Mongoose validation runs automatically before saving a document. Built-in validators include `required`, `min/max` (for numbers), `minlength/maxlength` (for strings), and `enum` (for allowed values). You can also define custom validators with functions. Schema middleware (pre/post hooks) lets you run logic before or after certain operations - the most common being a pre-save hook to hash passwords before storing them.
const mongoose = require('mongoose');
// Define a schema
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
minlength: [2, 'Name must be at least 2 characters']
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, 'Invalid email format']
},
age: {
type: Number,
min: [0, 'Age cannot be negative'],
max: 150
},
role: {
type: String,
enum: ['student', 'recruiter', 'admin'],
default: 'student'
},
skills: [String],
isActive: { type: Boolean, default: true }
}, {
timestamps: true // adds createdAt and updatedAt
});
// Add a virtual (computed property, not stored in DB)
userSchema.virtual('displayName').get(function() {
return `${this.name} (${this.role})`;
});
// Compile schema into a model
const User = mongoose.model('User', userSchema);
console.log('Schema fields:', Object.keys(userSchema.paths));
console.log('Model name:', User.modelName);
The schema defines each field's type, validation rules, and defaults. The timestamps option automatically manages createdAt/updatedAt fields. Virtuals are computed properties that exist on documents but are not persisted to the database.
const mongoose = require('mongoose');
// Assuming User model from previous example
async function crudExample() {
// Connect
await mongoose.connect('mongodb://localhost:27017/myapp');
console.log('Connected to MongoDB');
// CREATE
const user = await User.create({
name: 'Alice',
email: 'alice@example.com',
age: 25,
skills: ['JavaScript', 'Node.js']
});
console.log('Created:', user.name, user._id);
// READ
const found = await User.findById(user._id);
console.log('Found:', found.name);
const students = await User.find({ role: 'student' })
.sort('-createdAt')
.limit(10)
.select('name email');
console.log('Students:', students.length);
// UPDATE
const updated = await User.findByIdAndUpdate(
user._id,
{ $push: { skills: 'Express' }, age: 26 },
{ new: true, runValidators: true }
);
console.log('Updated skills:', updated.skills);
// DELETE
await User.findByIdAndDelete(user._id);
console.log('User deleted');
await mongoose.disconnect();
}
console.log('CRUD example function defined');
// crudExample().catch(console.error);
This shows all four CRUD operations. Note the options in findByIdAndUpdate: { new: true } returns the updated document instead of the original, and { runValidators: true } applies schema validation on updates (Mongoose skips validation on updates by default).
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
email: { type: String, required: true },
password: { type: String, required: true, minlength: 8 }
});
// Pre-save hook: hash password before storing
userSchema.pre('save', async function(next) {
// Only hash if password was modified
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
console.log('Password hashed before save');
next();
});
// Instance method: compare passwords
userSchema.methods.comparePassword = async function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
// Static method: find by email
userSchema.statics.findByEmail = function(email) {
return this.findOne({ email: email.toLowerCase() });
};
// Post-save hook: log after successful save
userSchema.post('save', function(doc) {
console.log('User saved:', doc.email);
});
const User = mongoose.model('User', userSchema);
console.log('Schema with hooks and methods defined');
console.log('Instance methods:', Object.keys(userSchema.methods));
console.log('Static methods:', Object.keys(userSchema.statics));
Pre-save middleware runs before each save, perfect for password hashing. Instance methods (schema.methods) are available on document instances. Static methods (schema.statics) are available on the model itself. The isModified check prevents re-hashing an already hashed password on updates.
MongoDB, Mongoose, schemas, models, CRUD, validation, virtuals, middleware