Mongoose middleware (pre and post hooks) explained
document middleware, query middleware, pre save hook, post save hook, pre find hook, this context, next() function, error handling in hooks
Document middleware hooks
Mongoose middleware lets you run code before (pre) or after (post) specific operations on a model. Document middleware hooks receive the document as this. Query middleware hooks receive the query object as this. Common uses: hashing passwords before save, writing audit logs after delete, and auto-filtering soft-deleted documents from all find queries.
const bcrypt = require('bcrypt')
// pre('save') — triggered before every .save() call
userSchema.pre('save', async function (next) {
// 'this' is the document being saved
if (!this.isModified('password')) return next() // skip if unchanged
this.password = await bcrypt.hash(this.password, 12)
next()
})
// post('save') — triggered after successful save
userSchema.post('save', function (doc, next) {
console.log(`User ${doc._id} saved successfully`)
next()
})
// Query middleware — intercepts all find-family operations
// /^find/ matches: find, findOne, findById, findOneAndUpdate, etc.
userSchema.pre(/^find/, function (next) {
// 'this' is the Query object — chain filter conditions here
this.find({ isDeleted: { $ne: true } })
next()
})Aborting an operation with an error
userSchema.pre('save', function (next) {
if (this.age < 0) return next(new Error('Age cannot be negative'))
next()
// This error rejects the save() promise in the calling code
})An important distinction: document middleware like pre('save') does not fire for operations like updateOne or updateMany called directly on the model — those bypass document middleware entirely. If you need hooks on update operations, use query middleware (pre('updateOne')) or switch to findById followed by document.save() which does trigger document hooks.
