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.x has 4 types of middleware: document middleware, model middleware, aggregate middleware, and query middleware. Document middleware is supported for the following document functions. In document middleware functions, this refers to the document.

Query middleware is supported for the following Model and Query functions. In query middleware functions, this refers to the query.

Aggregate middleware is for MyModel.aggregate(). Aggregate middleware executes when you call exec() on an aggregate object. In aggregate middleware, this refers to the aggregation object.

Model middleware is supported for the following model functions. In model middleware functions, this refers to the model.

All middleware types 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(). Note: The create() function fires save() hooks.

Pre

There are two types of pre hooks, serial and parallel.

Serial

Serial middleware functions are executed one after another, when each middleware calls next.

  1. var schema = new Schema(..);
  2. schema.pre('save', function(next) {
  3. // do stuff
  4. next();
  5. });

The next() call does not stop the rest of the code in your middleware function from executing. Use the early return pattern to prevent the rest of your middleware function from running when you call next().

  1. var schema = new Schema(..);
  2. schema.pre('save', function(next) {
  3. if (foo()) {
  4. console.log('calling next!');
  5. // `return next();` will make sure the rest of this function doesn't run
  6. /*return*/ next();
  7. }
  8. // Unless you comment out the `return` above, 'after next' will print
  9. console.log('after next');
  10. });

Parallel

Parallel middleware offer more fine-grained flow control.

  1. var schema = new Schema(..);
  2. // `true` means this is a parallel middleware. You **must** specify `true`
  3. // as the second parameter if you want to use parallel middleware.
  4. schema.pre('save', true, function(next, done) {
  5. // calling next kicks off the next middleware in parallel
  6. next();
  7. setTimeout(done, 100);
  8. });

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.

  1. schema.pre('save', function(next) {
  2. // You **must** do `new Error()`. `next('something went wrong')` will
  3. // **not** work
  4. var err = new Error('something went wrong');
  5. next(err);
  6. });
  7. // later...
  8. myDoc.save(function(err) {
  9. console.log(err.message) // something went wrong
  10. });

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.

  1. schema.post('init', function(doc) {
  2. console.log('%s has been initialized from the db', doc._id);
  3. });
  4. schema.post('validate', function(doc) {
  5. console.log('%s has been validated (but not saved yet)', doc._id);
  6. });
  7. schema.post('save', function(doc) {
  8. console.log('%s has been saved', doc._id);
  9. });
  10. schema.post('remove', function(doc) {
  11. console.log('%s has been removed', doc._id);
  12. });

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.

  1. // Takes 2 parameters: this is an asynchronous post hook
  2. schema.post('save', function(doc, next) {
  3. setTimeout(function() {
  4. console.log('post1');
  5. // Kick off the second post hook
  6. next();
  7. }, 10);
  8. });
  9. // Will not execute until the first middleware calls `next()`
  10. schema.post('save', function(doc, next) {
  11. console.log('post2');
  12. next();
  13. });

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.

  1. schema.pre('validate', function() {
  2. console.log('this gets printed first');
  3. });
  4. schema.post('validate', function() {
  5. console.log('this gets printed second');
  6. });
  7. schema.pre('save', function() {
  8. console.log('this gets printed third');
  9. });
  10. schema.post('save', function() {
  11. console.log('this gets printed fourth');
  12. });

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.

  1. schema.pre('find', function() {
  2. console.log(this instanceof mongoose.Query); // true
  3. this.start = Date.now();
  4. });
  5. schema.post('find', function(result) {
  6. console.log(this instanceof mongoose.Query); // true
  7. // prints returned documents
  8. console.log('find() returned ' + JSON.stringify(result));
  9. // prints number of milliseconds the query took
  10. console.log('find() took ' + (Date.now() - this.start) + ' millis');
  11. });

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.

  1. schema.pre('update', function() {
  2. this.update({},{ $set: { updatedAt: new Date() } });
  3. });

Error Handling Middleware

New in 4.5.0

Middleware execution normally stops the first time a piece of middleware calls next() with an error. However, there is a special kind of post middleware called “error handling middleware” that executes specifically when an error occurs.

Error handling middleware is defined as middleware that takes one extra parameter: the ‘error’ that occurred as the first parameter to the function. Error handling middleware can then transform the error however you want.

  1. var schema = new Schema({
  2. name: {
  3. type: String,
  4. // Will trigger a MongoError with code 11000 when
  5. // you save a duplicate
  6. unique: true
  7. }
  8. });
  9. // Handler **must** take 3 parameters: the error that occurred, the document
  10. // in question, and the `next()` function
  11. schema.post('save', function(error, doc, next) {
  12. if (error.name === 'MongoError' && error.code === 11000) {
  13. next(new Error('There was a duplicate key error'));
  14. } else {
  15. next(error);
  16. }
  17. });
  18. // Will trigger the `post('save')` error handler
  19. Person.create([{ name: 'Axl Rose' }, { name: 'Axl Rose' }]);

Error handling middleware also works with query middleware. You can also define a post update() hook that will catch MongoDB duplicate key errors.

  1. // The same E11000 error can occur when you call `update()`
  2. // This function **must** take 3 parameters. If you use the
  3. // `passRawResult` function, this function **must** take 4
  4. // parameters
  5. schema.post('update', function(error, res, next) {
  6. if (error.name === 'MongoError' && error.code === 11000) {
  7. next(new Error('There was a duplicate key error'));
  8. } else {
  9. next(error);
  10. }
  11. });
  12. var people = [{ name: 'Axl Rose' }, { name: 'Slash' }];
  13. Person.create(people, function(error) {
  14. Person.update({ name: 'Slash' }, { $set: { name: 'Axl Rose' } }, function(error) {
  15. // `error.message` will be "There was a duplicate key error"
  16. });
  17. });

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.