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 type | Typical operations | What this refers to | Good uses |
|---|---|---|---|
| Document | validate(), save(), selected document operations such as deleteOne() | The document instance | Normalize fields, compare old and new document values, enforce document-level rules |
| Query | find(), findOne(), updateOne(), findOneAndUpdate(), deleteMany() | The query object | Apply filters, inspect update operators, enforce query conventions |
| Aggregate | aggregate() | The aggregate object | Prepend pipeline stages, such as a tenant or soft-delete filter |
| Model | insertMany(), bulkWrite(), createCollection() | The model constructor | Batch 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
thisis 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 callnext()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.
- Write down the exact operation and the invariant it must enforce.
- Choose document, query, aggregate, or model middleware based on that operation.
- Register the hook before model compilation and verify its
thiscontext. - Test successful, invalid, asynchronous-failure, transaction, and bypass cases.
- 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.