Mongoose Middleware Guide: Document, Query, Aggregate, and Model Hooks
MongooseMongoDBMiddlewareHooksNode.js

Mongoose Middleware Guide: Document, Query, Aggregate, and Model Hooks

MMongoose Cloud Editorial Team
2026-11-10
5 min read

A practical Mongoose middleware checklist covering document, query, aggregate, and model hooks, registration, async errors, and testing.

Mongoose middleware lets you attach predictable behavior to model lifecycles, but the right hook depends on whether your code is validating a document, saving it, running a query, or executing an aggregation. This guide provides a reusable checklist for choosing document, query, aggregate, and model middleware, registering hooks correctly, handling asynchronous errors, and reviewing lifecycle rules before deployment.

Overview

Mongoose middleware, also called schema hooks, runs before or after selected Mongoose operations. Common uses include validation, audit fields, soft-delete filters, normalization, timestamps, and cleanup of related data.

The first decision is identifying the operation that must trigger the behavior. A save() hook is not a general-purpose hook for every write. For example, findOneAndUpdate() does not execute document save() middleware, because Mongoose is updating a query rather than loading and saving a document.

Middleware typeTypical operationsWhat this refers toGood uses
Documentvalidate(), save(), selected document operations such as deleteOne()The document instanceNormalize fields, compare old and new document values, enforce document-level rules
Queryfind(), findOne(), updateOne(), findOneAndUpdate(), deleteMany()The query objectApply filters, inspect update operators, enforce query conventions
Aggregateaggregate()The aggregate objectPrepend pipeline stages, such as a tenant or soft-delete filter
ModelinsertMany(), bulkWrite(), createCollection()The model constructorBatch operations and collection-level preparation

Hook availability and defaults can vary by Mongoose version and by operation. Check the version-specific documentation when upgrading, especially for less common hooks such as deleteOne, bulkWrite, and document-versus-query registration.

Checklist by scenario

Validation and normalization before saving

Use pre('validate') when a value must be prepared before schema validation, and pre('save') when the rule belongs to persistence. Register hooks on the schema before compiling the model.

const userSchema = new mongoose.Schema({
  email: { type: String, required: true },
  emailNormalized: String
});

userSchema.pre('validate', function(next) {
  this.email = this.email.trim();
  next();
});

userSchema.pre('save', function() {
  this.emailNormalized = this.email.toLowerCase();
});

const User = mongoose.model('User', userSchema);

Use an ordinary function rather than an arrow function when you need this to be the document. Avoid placing irreversible side effects in validation hooks; validation may fail before persistence.

Auditing and timestamps

A document pre('save') hook can record values available on the document, such as the actor or a revision marker. However, teams that write through both save() and update methods need separate query middleware or an explicit service-layer policy. A save hook alone will not cover every write path.

Query-based updates

Use query middleware for operations such as findOneAndUpdate() and updateOne(). In these hooks, this is the query, not the document.

userSchema.pre('findOneAndUpdate', function() {
  this.set({ updatedAt: new Date() });
});

userSchema.pre('findOneAndUpdate', function() {
  const update = this.getUpdate();
  // Inspect or adjust $set, $inc, and other update operators here.
});

If the rule needs the existing document, decide whether to load it explicitly, use a document-based workflow, or make the query operation’s limitations clear. Do not assume that this.email exists in a query hook.

Soft deletes

A common pattern is to add isDeleted: false or a deletion timestamp and exclude deleted records from reads. Query middleware can apply the filter to the read methods your application actually uses:

userSchema.pre(/^find/, function() {
  this.where({ deletedAt: null });
});

Review this pattern carefully before using it broadly. It may affect administrative queries, population behavior, counting methods, and reporting paths. Provide an intentional way to include deleted records rather than relying on an accidental bypass.

Aggregation pipelines

Aggregation middleware operates on an aggregate object. You can inspect or modify the pipeline with this.pipeline():

userSchema.pre('aggregate', function() {
  this.pipeline().unshift({ $match: { deletedAt: null } });
});

Confirm that the inserted stage is compatible with every pipeline shape. A filter that is safe at the beginning of one pipeline may need different placement when a pipeline starts with a transformation or uses a special stage.

Batch and model operations

Use model middleware for operations such as insertMany() or bulkWrite() when the behavior belongs to the batch operation itself. Test whether the validation, defaults, and hooks you expect are applied for that particular method; do not infer behavior from save() middleware.

What to double-check

  • Registration order: Define every hook before calling mongoose.model(). Hooks added afterward may not be registered for the compiled model.
  • Operation coverage: List every write path: save(), create(), insertMany(), updateOne(), findOneAndUpdate(), and direct driver calls. Select hooks for the paths you actually use.
  • Context: Confirm whether this is a document, query, aggregate, or model. Log the operation during development if the context is unclear.
  • Async completion: In an async hook, await required work and throw or reject on failure. In callback-style middleware, call next(error) and do not call next() twice.
  • Execution order: Check whether validation, document middleware, query middleware, and post middleware run in the sequence your rule assumes. Keep dependent behavior in one clearly ordered chain.
  • Transactions and side effects: If a hook sends an email, publishes an event, or writes to another system, determine what happens when the database transaction rolls back or the operation is retried.
  • Tests: Add tests for each supported operation and for failure cases. The Mongoose model testing guide can help structure unit and integration coverage.

Common mistakes

The most frequent mistake is expecting a pre('save') hook to run for findOneAndUpdate(). Another is using an arrow function and losing the Mongoose-bound this. A third is reading document fields inside query middleware without first retrieving the document.

Be cautious with broad regular expressions such as /^find/. They can affect methods you did not intend to change. Similarly, a soft-delete hook that covers reads but not aggregations can produce inconsistent results. Keep bypasses explicit, document them, and test privileged and reporting queries separately.

Hooks are also a poor place for hidden business workflows that span several models. If the rule requires a transaction, coordinated retries, or a visible command boundary, an application service may be easier to reason about. Review related write semantics in Mongoose update methods compared and transaction behavior in the Mongoose transactions tutorial.

When to revisit

Revisit this checklist before adding a lifecycle rule, changing a write method, introducing soft deletes, or moving from single-document writes to bulk operations. Also review it during seasonal planning cycles when retention, audit, or tenant-isolation workflows are updated, and whenever your application upgrades to a new major Mongoose version.

  1. Write down the exact operation and the invariant it must enforce.
  2. Choose document, query, aggregate, or model middleware based on that operation.
  3. Register the hook before model compilation and verify its this context.
  4. Test successful, invalid, asynchronous-failure, transaction, and bypass cases.
  5. Inspect all alternate write paths before deploying the schema change.

Keep this checklist beside schema changes and update it when workflows or tools change. Middleware is most reliable when its scope is explicit, its execution order is tested, and its limitations are visible to the next developer.

Related Topics

#Mongoose#MongoDB#Middleware#Hooks#Node.js
M

Mongoose Cloud Editorial Team

Developer Tools Editor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.