How to Implement Mongoose Middleware for Improved Data Handling
In Mongoose, middleware is a way to define custom logic to run before or after certain actions in your MongoDB schema, like saving a document, updating, deleting, or even finding. Middleware functions allow you to add specific behavior to these operations, making it easier to enforce rules, perform validations, log activities, or modify data automatically
Types of Middleware
1. Document Middleware
Description: Document middleware applies directly to individual documents and affects operations such as saving, validating, and removing.
Common Methods:
save
,validate
,remove
,init
.Example:
const mongoose = require('mongoose'); const bcrypt = require('bcrypt'); // Define the User schema const userSchema = new mongoose.Schema({ username: { type: String, required: [true, 'Username is required'], unique: true, minlength: [3, 'Username must be at least 3 characters long'] }, password: { type: String, required: [true, 'Password is required'], minlength: [8, 'Password must be at least 8 characters long'] }, email: { type: String, required: [true, 'Email is required'], unique: true, match: [/\S+@\S+\.\S+/, 'Please use a valid email address'] } }); // Pre-save middleware to hash the password userSchema.pre('save', async function (next) { // Only hash the password if it has been modified (or is new) if (this.isModified('password')) { try { // Generate a salt const salt = await bcrypt.genSalt(10); // Hash the password with the salt this.password = await bcrypt.hash(this.password, salt); next(); } catch (err) { next(err); // Pass the error to the next middleware } } else { next(); } }); // Custom validation before save: Check if password contains at least one number userSchema.pre('save', function (next) { const passwordRegex = /[0-9]/; // Password must contain at least one digit if (!passwordRegex.test(this.password)) { return next(new Error('Password must contain at least one number.')); } next(); }); // Create the User model const User = mongoose.model('User', userSchema); module.exports = User;
Use Cases: Modifying or validating data before saving it to the database, like hashing passwords before
save
or performing additional validation checks.
2. Model Middleware
Description: Model middleware, also known as static middleware, applies to model-level operations that do not directly involve a specific document instance.
Common Methods:
insertMany
.Example:
userSchema.pre('insertMany', function(next, docs) { console.log('Model middleware for insertMany'); next(); });
Use Cases: Useful for handling bulk operations, like validating data or adding default values when inserting multiple documents.
3. Aggregate Middleware
Description: Aggregate middleware is specific to MongoDB aggregation pipelines. It allows you to modify or inspect the aggregation pipeline before it’s executed.
Common Methods:
aggregate
.Example:
userSchema.pre('aggregate', function(next) { // Modify the aggregation pipeline before execution this.pipeline().unshift({ $match: { isActive: true } }); console.log('Aggregate middleware'); next(); });
Use Cases: Modifying the aggregation pipeline for things like filtering out inactive documents or adding additional stages based on dynamic conditions.
4. Query Middleware
Description: Query middleware applies to operations that retrieve, update, or delete documents based on queries.
Common Methods:
find
,findOne
,findOneAndUpdate
,update
,deleteOne
, and many more.Example:
userSchema.pre('find', function(next) { this.where({ isActive: true }); // Only find active users console.log('Query middleware for find'); next(); });
Use Cases: Adding query conditions automatically (e.g., applying a default filter), logging query operations, or modifying query results before returning.
There are two main types of middleware in Mongoose:
Pre Middleware: Executes before a specific action.
Often used for actions like validation, data transformation, or logging before saving, updating, or finding documents.
Example: Encrypting a password before saving a user document.
When the pre('save')
Hook is Called
Document Creation or Update:
- The
pre('save')
hook is triggered whenever a document is saved using thesave()
method. This can happen either when a new document is being created or an existing document is being modified and saved.
- The
Only on Explicit
save()
Calls:The
pre('save')
middleware is only called when you explicitly call.save()
on a document. For example:const user = new User({ username: 'testuser', password: 'securePassword1' }); await user.save(); // This triggers the pre('save') middleware
It does not trigger for other methods like
updateOne
,findOneAndUpdate
, orinsertMany
. For those, you would need to use different hooks, such aspre('updateOne')
orpre('insertMany')
.
Conditionally Based on Document Modifications:
The
pre('save')
hook is only triggered if certain fields in the document are modified (or if it’s a new document).For example, in the context of hashing passwords, you often only want to rehash the password if it has been modified. You can check this with
this.isModified('fieldName')
:userSchema.pre('save', function (next) { if (this.isModified('password')) { // Only rehash the password if it’s modified } next(); });
How the pre('save')
Hook Works Internally
When you call
.save()
on a document, Mongoose goes through a series of steps to prepare the document for insertion or updating in MongoDB.The
pre('save')
hook is called just before Mongoose begins interacting with the database. Any changes you make to the document in thepre('save')
hook (such as modifying a field value) will be included in the final saved version.If there’s an error in the
pre('save')
hook (for example, a validation failure), Mongoose stops the operation and does not save the document to the database.
Example Workflow of pre('save')
Hook
Define Document:
const user = new User({ username: 'exampleUser', password: 'plainTextPassword' });
Trigger
pre('save')
Middleware:- When you call
user.save
()
, Mongoose checks if there’s apre('save')
hook on the schema and runs it before saving.
- When you call
Execute Hook Logic:
If the
pre('save')
hook modifiesuser.password
(e.g., hashes it), the modified version will be saved.If the hook encounters an error (e.g., custom validation failure), the save process is interrupted, and the error is returned.
Save Document:
- If there are no errors, the document is saved with all modifications from the
pre('save')
middleware applied.
- If there are no errors, the document is saved with all modifications from the
What Happens If You Don’t Call .save()
If you create or modify a document without calling .save()
, the pre('save')
middleware won’t run, and your changes won’t be saved to the database. For example:
const user = new User({
username: 'exampleUser',
password: 'plainTextPassword'
});
// If you don't call user.save(), the document won't be saved, and the pre('save') middleware won't run.
Triggers:
pre('save')
: Runs before saving a document.pre('validate')
: Runs before Mongoose’s validation step.pre('remove')
: Runs before removing a document.
pre('find')
: Runs before afind
query.pre('findOne')
: Runs before afindOne
query.pre('findOneAndUpdate')
: Runs before an update query that finds and updates a single document.pre('updateOne')
: Runs before updating a single document.pre('deleteOne')
: Runs before deleting a single document.pre('insertMany')
: Runs before bulk insertion of documents usinginsertMany
.pre('aggregate')
: Runs before an aggregation pipeline is executed.
Summary Table
Pre Middleware Type | Trigger Events | Common Use Case |
Document Pre | save , remove , validate | Hash passwords, data validation, logging |
Query Pre | find , findOne , findOneAndUpdate , updateOne , deleteOne | Add filters, enforce rules, modify queries |
Model Pre | insertMany | Modify bulk insert documents, add defaults |
Aggregate Pre | aggregate | Add stages to aggregation pipeline, logging |
Post Middleware: Executes after a specific action.
Useful for handling the result of a completed operation, like logging or sending a notification after a document is saved.
Example: Sending an email after a new user is created.
Types of Post Middleware
Mongoose supports post middleware for documents, models, queries, and aggregates. Let’s go over how these work and when they’re used.
1. Document Post Middleware
Description: Runs after actions like
save
,remove
, andvalidate
on a document instance.Common Use Case: Logging actions, sending notifications, or performing follow-up actions after saving, deleting, or validating a document.
Example: post('save')
userSchema.post('save', function(doc) {
console.log(`New user ${doc.username} was saved.`);
// Additional logic, like sending a welcome email, can go here
});
In this example, the post('save')
hook runs after a document is successfully saved.
2. Query Post Middleware
Description: Runs after query operations like
find
,findOne
,findOneAndUpdate
,updateOne
, anddeleteOne
.Common Use Case: Modifying or logging the result of a query after it completes, such as filtering results or logging query actions.
Example: post('find')
userSchema.post('find', function(docs) {
console.log(`${docs.length} users found`);
// Additional actions, like formatting data, can go here
});
In this example, the post('find')
hook runs after a find
query is executed and receives the array of matching documents (docs
) as an argument.
3. Model Post Middleware
Description: Runs after model-specific operations like
insertMany
.Common Use Case: Verifying or modifying the results of bulk operations, such as logging inserted documents or performing cleanup after bulk inserts.
Example: post('insertMany')
userSchema.post('insertMany', function(docs) {
console.log(`${docs.length} users were inserted.`);
});
This post('insertMany')
middleware runs after a bulk insert operation is completed.
4. Aggregate Post Middleware
Description: Runs after an aggregation pipeline is executed.
Common Use Case: Adjusting or logging the aggregation results before they’re returned to the application.
Example: post('aggregate')
userSchema.post('aggregate', function(result) {
console.log('Aggregation result:', result);
});
In this example, the post('aggregate')
hook runs after an aggregation pipeline and logs the final result.
Summary Table
Middleware Type | Trigger Events | Example Usage |
Document Post | save , remove , validate | Logging, sending notifications after saving or deleting |
Query Post | find , findOne , updateOne | Modifying results or logging query results |
Model Post | insertMany | Logging, performing actions after bulk insertions |
Aggregate Post | aggregate | Adjusting or logging aggregation results |
Subscribe to my newsletter
Read articles from Priyanshu Pandey directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by