Mongoose Middleware: Automating Schema Logic with Pre‑ and Post‑Hooks
Mongoose middleware lets you run code automatically on document lifecycle events. This guide shows how to use pre‑save hooks for data normalization, post‑save hooks for side effects, and query middleware for updates. It also covers common pitfalls and when to avoid middleware.
23 Sept 2026, 02:59 UTC

Why Use Mongoose Middleware?
When building a Node.js application that stores data in MongoDB, you often need to run code automatically when a document is created, updated, or removed. Mongoose middleware lets you attach functions to these lifecycle events so you can enforce data integrity, hash passwords, or trigger notifications without scattering logic throughout your codebase.
The most common hooks are pre('save') and post('save'). A pre('save') hook runs before a document is written to the database, making it ideal for normalizing data. A post('save') hook runs after the write succeeds, which is perfect for side effects that shouldn’t block the main operation.
Working Example: Hashing Passwords and Logging Saves
Below is a minimal user schema that demonstrates the two hooks. Replace <YOUR_HASH_FUNCTION> with your hashing library (e.g., bcrypt).
const mongoose = require('mongoose');
const { Schema } = mongoose;
const userSchema = new Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
createdAt: { type: Date, default: Date.now }
});
// Pre‑save: hash the password if it has changed
userSchema.pre('save', async function (next) {
const user = this; // the document being saved
if (!user.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(user.password, salt);
next();
} catch (err) {
next(err);
}
});
// Post‑save: log the action
userSchema.post('save', function (doc, next) {
console.log(`User ${doc.email} was created at ${doc.createdAt}`);
next();
});
module.exports = mongoose.model('User', userSchema);
To verify the hooks work:
- Create a user:
const u = new User({ email: '[contact removed]', password: 'plain' }); await u.save(); - Check the database: the stored
passwordfield should be a hashed string, not "plain". - Observe the console: the log message should appear after the write.
Document vs. Query Middleware
Mongoose distinguishes between document middleware (e.g., save, remove) that act on a single instance, and query middleware that intercepts query operations like findOneAndUpdate or deleteMany. The syntax is similar but the function signature differs.
// Query middleware example
userSchema.pre('findOneAndUpdate', function (next) {
const update = this.getUpdate();
if (update.password) {
// Hash new password before update
const salt = bcrypt.genSaltSync(10);
update.password = bcrypt.hashSync(update.password, salt);
}
next();
});
Remember that query middleware does not receive the document instance; you must inspect and modify the update object directly.
Common Mistakes and Limitations
- Forgetting
next(): In non‑async middleware, failing to callnext()stalls the operation. Always invoke it unless you intentionally abort. - Assuming post hooks modify the document: Changes made to
docinside apost('save')hook are not persisted. If you need to update the document after a write, usepre('save')or a separate update call. - Overusing pre hooks for heavy work: Complex business logic in pre hooks can make debugging hard and slow down writes. Keep them focused on data integrity.
- Missing query middleware for updates:
pre('save')does not fire onupdateMany()orfindOneAndUpdate(). If you need validation on bulk updates, add a query middleware or perform validation in your service layer. - Assuming middleware order: Mongoose executes middleware in the order they’re added. If you rely on a specific sequence, add hooks in that order or use
pre('save', { document: true }, fn)to enforce document-level execution.
When Not to Use Middleware
If your application requires high write throughput and the logic is trivial, inline code may be faster because middleware adds a function call overhead. For complex pipelines that involve external services (e.g., sending emails), consider off‑loading to a message queue instead of blocking the write with a post hook.
Final Checklist for Production Use
- Define clear responsibilities: use pre hooks for data validation/normalization, post hooks for side effects.
- Test hooks independently: create a unit test that creates a document and asserts the expected state.
- Document each hook’s purpose in the schema file to aid future maintainers.
- Monitor performance: log hook execution time in staging to catch regressions.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.