← Back to list

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…

Afridi Akbar Ifty · 2026-04-25 14:01 · 0 claps · 4.0 min read
#mongoose #middleware
Open on Medium ↗
Wiki topics: 🌐 · Web Development

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:

  1. Document Middleware
  2. Query Middleware
  3. 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 find queries
  • Timestamps — Manually set createdAt or updatedAt before 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 pre to run logic before an operation and post to run logic after
  • Always use regular functions (not arrow functions) when you need access to this
  • Use async/await in middleware for cleaner asynchronous code — no need to call next() manually
  • You can abort operations by passing an error to next(error) in pre hooks
  • 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