Middleware
Middleware (also called pre and post hooks) are functions which are passed control during execution of asynchronous functions. Middleware is specified on the schema level and is useful for writing plugins. Mongoose 4.0 has 2 types of middleware: document middleware and query middleware. Document middleware is supported for the following document functions.
Query middleware is supported for the following Model and Query functions.
Both document middleware and query middleware support pre and post hooks. How pre and post hooks work is described in more detail below.
Note: There is no query hook for remove()
, only for documents.
If you set a 'remove' hook, it will be fired when you call myDoc.remove()
,
not when you call MyModel.remove()
.
Pre
There are two types of pre
hooks, serial and parallel.
Serial
Serial middleware are executed one after another, when each middleware calls next
.
var schema = new Schema(..);
schema.pre('save', function(next) {
// do stuff
next();
});
Parallel
Parallel middleware offer more fine-grained flow control.
var schema = new Schema(..);
// `true` means this is a parallel middleware. You **must** specify `true`
// as the second parameter if you want to use parallel middleware.
schema.pre('save', true, function(next, done) {
// calling next kicks off the next middleware in parallel
next();
setTimeout(done, 100);
});
The hooked method, in this case save
, will not be executed until done
is called by each middleware.
Use Cases
Middleware are useful for atomizing model logic and avoiding nested blocks of async code. Here are some other ideas:
- complex validation
- removing dependent documents
- (removing a user removes all his blogposts)
- asynchronous defaults
- asynchronous tasks that a certain action triggers
- triggering custom events
- notifications
Error handling
If any middleware calls next
or done
with a parameter of type Error
,
the flow is interrupted, and the error is passed to the callback.
schema.pre('save', function(next) {
// You **must** do `new Error()`. `next('something went wrong')` will
// **not** work
var err = new Error('something went wrong');
next(err);
});
// later...
myDoc.save(function(err) {
console.log(err.message) // something went wrong
});
Post middleware
post middleware are executed after
the hooked method and all of its pre
middleware have completed.
post
middleware do not directly receive flow control, e.g. no next
or
done
callbacks are passed to it. post
hooks are a way to register
traditional event listeners for these methods.
schema.post('init', function(doc) {
console.log('%s has been initialized from the db', doc._id);
});
schema.post('validate', function(doc) {
console.log('%s has been validated (but not saved yet)', doc._id);
});
schema.post('save', function(doc) {
console.log('%s has been saved', doc._id);
});
schema.post('remove', function(doc) {
console.log('%s has been removed', doc._id);
});
Asynchronous Post Hooks
While post middleware doesn't receive flow control, you can still make
sure that asynchronous post hooks are executed in a pre-defined order.
If your post hook function takes at least 2 parameters, mongoose will
assume the second parameter is a next()
function that you will call to
trigger the next middleware in the sequence.
// Takes 2 parameters: this is an asynchronous post hook
schema.post('save', function(doc, next) {
setTimeout(function() {
console.log('post1');
// Kick off the second post hook
next();
}, 10);
});
// Will not execute until the first middleware calls `next()`
schema.post('save', function(doc, next) {
console.log('post2');
next();
});
Save/Validate Hooks
The save()
function triggers validate()
hooks, because mongoose
has a built-in pre('save')
hook that calls validate()
. This means
that all pre('validate')
and post('validate')
hooks get called
before any pre('save')
hooks.
schema.pre('validate', function() {
console.log('this gets printed first');
});
schema.post('validate', function() {
console.log('this gets printed second');
});
schema.pre('save', function() {
console.log('this gets printed third');
});
schema.post('save', function() {
console.log('this gets printed fourth');
});
Notes on findAndUpdate() and Query Middleware
Pre and post save()
hooks are not executed on update()
,
findOneAndUpdate()
, etc. You can see a more detailed discussion why in
this GitHub issue.
Mongoose 4.0 has distinct hooks for these functions.
schema.pre('find', function() {
console.log(this instanceof mongoose.Query); // true
this.start = Date.now();
});
schema.post('find', function(result) {
console.log(this instanceof mongoose.Query); // true
// prints returned documents
console.log('find() returned ' + JSON.stringify(result));
// prints number of milliseconds the query took
console.log('find() took ' + (Date.now() - this.start) + ' millis');
});
Query middleware differs from document middleware in a subtle but
important way: in document middleware, this
refers to the document
being updated. In query middleware, mongoose doesn't necessarily have
a reference to the document being updated, so this
refers to the
query object rather than the document being updated.
For instance, if you wanted to add an updatedAt
timestamp to every
update()
call, you would use the following pre hook.
schema.pre('update', function() {
this.update({},{ $set: { updatedAt: new Date() } });
});
Next Up
Now that we've covered middleware, let's take a look at Mongoose's approach to faking JOINs with its query population helper.