Mongoose Middleware and Hooks are powerful features that allow executing custom logic before or after certain operations. Middleware is divided into two categories: Document Middleware and Query Middleware.
Middleware Types
1. Document Middleware
Operations performed on document instances, such as save(), validate(), remove(), etc.
javascriptuserSchema.pre('save', function(next) { console.log('About to save user:', this.name); next(); }); userSchema.post('save', function(doc) { console.log('User saved:', doc.name); });
2. Query Middleware
Operations performed on Model queries, such as find(), findOne(), updateOne(), etc.
javascriptuserSchema.pre('find', function() { this.where({ deleted: false }); }); userSchema.post('find', function(docs) { console.log('Found', docs.length, 'users'); });
Common Hooks
Document Operation Hooks
validate- Validate documentsave- Save documentremove- Remove documentinit- Initialize document (load from database)
Query Operation Hooks
count- Count queryfind- Find documentsfindOne- Find single documentfindOneAndDelete- Find and deletefindOneAndUpdate- Find and updateupdateOne- Update single documentupdateMany- Update multiple documentsdeleteOne- Delete single documentdeleteMany- Delete multiple documents
Difference Between Pre and Post Hooks
Pre Hooks
- Run before operation execution
- Can modify data or abort operation
- Must call
next()or return Promise - Can access
this(document instance or query object)
javascriptuserSchema.pre('save', function(next) { if (this.age < 0) { const err = new Error('Age cannot be negative'); return next(err); } this.email = this.email.toLowerCase(); next(); });
Post Hooks
- Run after operation execution
- Cannot modify data or abort operation
- Receive operation result as parameter
- Can access
this(document instance or query object)
javascriptuserSchema.post('save', function(doc) { console.log('User saved with ID:', doc._id); // Send notifications, log, etc. });
Async Middleware
Mongoose middleware supports async operations:
javascript// Using async/await userSchema.pre('save', async function(next) { const existing = await this.constructor.findOne({ email: this.email }); if (existing && existing._id.toString() !== this._id.toString()) { const err = new Error('Email already exists'); return next(err); } next(); }); // Return Promise userSchema.pre('save', function() { return checkEmailAvailability(this.email).then(isAvailable => { if (!isAvailable) { throw new Error('Email already exists'); } }); });
Practical Use Cases
- Password Hashing: Encrypt password before saving user
- Timestamps: Automatically set createdAt and updatedAt
- Soft Delete: Mark as deleted before actual deletion
- Data Validation: Execute complex validation logic
- Logging: Record operation history
- Cache Invalidation: Update related caches
- Related Data: Automatically update related documents
- Notifications: Send notifications after operations
Important Notes
- Middleware executes in definition order
- Errors in
prehooks abort the operation - Query middleware doesn't trigger document middleware
- Use
{ runValidators: true }withfindOneAndUpdateto trigger validation - Avoid infinite loops in middleware