Understanding Mongoose Middlewares (Hooks) — Pre and Post in Action
When working with Mongoose, a popular ODM (Object Document Mapper) for MongoDB and Node.js, it’s essential to understand how middleware…
Understanding Mongoose Middlewares (Hooks) — Pre and Post in Action
When working with Mongoose, a popular ODM (Object Document Mapper) for MongoDB and Node.js, it’s essential to understand how middleware (also known as hooks) work. Middleware functions are an indispensable part of writing clean, maintainable, and modular Mongoose code.

In this post, we’ll dive deep into:
- What Mongoose middleware is
- Types of middleware: Document, Query, and Aggregation
- The difference between pre and post hooks
- Best practices and usage examples
What is Mongoose Middleware?
In Mongoose, middleware (or hooks) are functions that are passed control before or after certain lifecycle events. These are tied to the schema and let you plug into Mongoose’s flow.
Think of them as events or checkpoints where you can run code:
- before saving a document
- after a query
- before aggregation, etc.
Mongoose middlewares are categorized into:
- Document Middleware
- Query Middleware
- Aggregate Middleware
Let’s understand these with examples.
1. Document Middleware
Document middleware runs during document save and remove operations.
// Syntax:
schema.pre('save', function (next) {
// logic before saving
next();
});
schema.post('save', function (doc) {
// logic after saving
});
schema.pre('remove', function (next) {
// logic before removing
next();
});
schema.post('remove', function (doc) {
// logic after removing
});
Important: You must use regular functions, not arrow functions (=>) here. Why? Because arrow functions don't bind their own this, and this is critical inside middleware to refer to the document instance.
// Example:
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: String,
email: String,
});
userSchema.pre('save', function (next) {
console.log('Before saving:', this.name);
this.name = this.name.trim(); // cleaning up name
next();
});
userSchema.post('save', function (doc) {
console.log('After saving:', doc);
});
userSchema.pre('remove', function (next) {
console.log(`Preparing to remove ${this.name}`);
next();
});
userSchema.post('remove', function (doc) {
console.log(`Removed: ${doc.name}`);
});
const User = mongoose.model('User', userSchema);
2. Query Middleware
This middleware is executed before or after query methods like find, findOne, updateOne, etc.
// Syntax:
schema.pre('find', function (next) {
// logic before a find query
next();
});
schema.post('find', function (docs) {
// logic after a find query
});
schema.pre('findOne', function (next) {
next();
});
schema.post('findOne', function (doc) {
// doc is the found document
});
// Example:
const productSchema = new mongoose.Schema({
name: String,
price: Number,
isDeleted: { type: Boolean, default: false },
});
// Automatically exclude soft-deleted documents from all find queries
productSchema.pre('find', function (next) {
this.where({ isDeleted: false });
next();
});
productSchema.pre('findOne', function (next) {
this.where({ isDeleted: false });
next();
});
productSchema.post('find', function (docs) {
console.log(`Found ${docs.length} products`);
});
productSchema.post('findOne', function (doc) {
if (doc) {
console.log(`Found product: ${doc.name}`);
} else {
console.log('No product found');
}
});
const Product = mongoose.model('Product', productSchema);
Note: In query middleware, this refers to the query object, not the document. That's why you call this.where(...) to add conditions to the query.
3. Aggregate Middleware
Aggregate middleware runs before or after aggregation pipelines. It’s useful when you want to consistently inject pipeline stages — for example, always filtering out soft-deleted documents in aggregations.
// Syntax:
schema.pre('aggregate', function (next) {
// logic before aggregation
next();
});
schema.post('aggregate', function (result) {
// logic after aggregation
});
// Example:
const orderSchema = new mongoose.Schema({
product: String,
quantity: Number,
isDeleted: { type: Boolean, default: false },
});
// Inject a $match stage at the beginning of every aggregation pipeline
orderSchema.pre('aggregate', function (next) {
this.pipeline().unshift({ $match: { isDeleted: false } });
next();
});
orderSchema.post('aggregate', function (result) {
console.log(`Aggregation returned ${result.length} results`);
});
const Order = mongoose.model('Order', orderSchema);
Note: In aggregate middleware, this refers to the aggregation object. You can access and modify the pipeline using this.pipeline().
Pre vs Post — What’s the Difference?
Feature pre Hook post Hook When it runs Before the operation After the operation Access to next Yes (must call next()) No (optional next in some cases) Can abort operation Yes (pass an error to next) Limited Common use cases Validation, transformation, logging Logging, notifications, cleanup this in Document MW The document instance The saved/removed document this in Query MW The query object The result(s)
Async/Await in Middleware
You can also write middleware using async/await instead of calling next() manually:
userSchema.pre('save', async function () {
// No need to call next() with async functions
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});
Mongoose handles the promise automatically when you use async functions — just make sure not to mix async with next() incorrectly.
Error Handling in Middleware
You can pass errors to next() to abort the operation and propagate the error:
userSchema.pre('save', function (next) {
if (!this.email.includes('@')) {
return next(new Error('Invalid email address'));
}
next();
});
For post hooks, Mongoose also supports error-handling middleware with a special signature:
userSchema.post('save', function (error, doc, next) {
if (error.name === 'MongoServerError' && error.code === 11000) {
next(new Error('Email already exists'));
} else {
next(error);
}
});
Real-World Use Cases
- Password hashing — Hash a user’s password before saving using
pre('save') - Soft delete filtering — Automatically exclude deleted records from all
findqueries - Timestamps — Manually set
createdAtorupdatedAtbefore saving - Audit logging — Log every document update or deletion after it occurs
- Cascading deletes — Remove related documents when a parent is deleted using
pre('remove') - Data normalization — Trim strings, normalize emails to lowercase, etc. before saving
Key Takeaways
- Mongoose middleware lets you hook into the lifecycle of documents, queries, and aggregations
- Use
preto run logic before an operation andpostto run logic after - Always use regular functions (not arrow functions) when you need access to
this - Use
async/awaitin middleware for cleaner asynchronous code — no need to callnext()manually - You can abort operations by passing an error to
next(error)inprehooks - Middleware is defined on the schema, so it must be added before calling
mongoose.model()
Mongoose middleware is a powerful pattern that keeps your data logic centralized and your controllers clean. Once you get comfortable with hooks, you’ll find yourself reaching for them constantly — from hashing passwords to enforcing soft deletes across your entire application.
Happy coding! 🚀
메타데이터
- post_id
- 71488f3cd4e4
- slug
- understanding-mongoose-middlewares-hooks-pre-and-post-in-action-71488f3cd4e4
- url
- https://medium.com/@iamafridi/understanding-mongoose-middlewares-hooks-pre-and-post-in-action-71488f3cd4e4
- canonical_url
- https://medium.com/@iamafridi/understanding-mongoose-middlewares-hooks-pre-and-post-in-action-71488f3cd4e4
- author_url
- https://medium.com/@iamafridi
- status
- ok
- fetched_at
- 2026-06-23 17:05:31