Mongoose 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, 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: If you specify schema.pre('remove'), Mongoose will register this middleware for by default. If you want to your middleware to run on Query.remove() use .

    Note: Unlike schema.pre('remove'), Mongoose registers updateOne and deleteOne middleware on Query#updateOne() and Query#deleteOne() by default. This means that both doc.updateOne() and Model.updateOne() trigger updateOne hooks, but this refers to a query, not a document. To register updateOne or deleteOne middleware as document middleware, use schema.pre('updateOne', { document: true, query: false }).

    Note: The create() function fires save() hooks.

    Pre

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

    In , instead of calling next() manually, you can use a function that returns a promise. In particular, you can use async/await.

    1. schema.pre('save', function() {
    2. return doStuff().
    3. then(() => doMoreStuff());
    4. });
    5. // Or, in Node.js >= 7.6.0:
    6. schema.pre('save', async function() {
    7. await doStuff();
    8. await doMoreStuff();
    9. });

    If you use next(), the next() call does not stop the rest of the code in your middleware function from executing. Use to prevent the rest of your middleware function from running when you call next().

    1. const 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. });

    • complex validation
    • removing dependent documents (removing a user removes all their blogposts)
    • asynchronous defaults
    • asynchronous tasks that a certain action triggers

    If any pre hook errors out, mongoose will not execute subsequent middleware or the hooked function. Mongoose will instead pass an error to the callback and/or reject the returned promise. There are several ways to report an error in middleware:

    1. schema.pre('save', function(next) {
    2. const err = new Error('something went wrong');
    3. // If you call `next()` with an argument, that argument is assumed to be
    4. // an error.
    5. next(err);
    6. });
    7. schema.pre('save', function() {
    8. // You can also return a promise that rejects
    9. return new Promise((resolve, reject) => {
    10. reject(new Error('something went wrong'));
    11. });
    12. });
    13. schema.pre('save', function() {
    14. // You can also throw a synchronous error
    15. });
    16. schema.pre('save', async function() {
    17. await Promise.resolve();
    18. // You can also throw an error in an `async` function
    19. throw new Error('something went wrong');
    20. });
    21. // later...
    22. // Changes will not be persisted to MongoDB because a pre hook errored out
    23. myDoc.save(function(err) {
    24. console.log(err.message); // something went wrong
    25. });

    Calling next() multiple times is a no-op. If you call next() with an error err1 and then throw an error err2, mongoose will report err1.

    post middleware are executed after the hooked method and all of its pre middleware have completed.

    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

    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. setTimeout(function() {
    3. console.log('post1');
    4. // Kick off the second post hook
    5. next();
    6. }, 10);
    7. });
    8. // Will not execute until the first middleware calls `next()`
    9. schema.post('save', function(doc, next) {
    10. console.log('post2');
    11. next();
    12. });

    Calling pre() or post() after does not work in Mongoose in general. For example, the below pre('save') middleware will not fire.

    1. const schema = new mongoose.Schema({ name: String });
    2. // Compile a model from the schema
    3. const User = mongoose.model('User', schema);
    4. // Mongoose will **not** call the middleware function, because
    5. // this middleware was defined after the model was compiled
    6. schema.pre('save', () => console.log('Hello from pre save'));
    7. new User({ name: 'test' }).save();

    This means that you must add all middleware and plugins before calling . The below script will print out “Hello from pre save”:

    As a consequence, be careful about exporting Mongoose models from the same file that you define your schema. If you choose to use this pattern, you must define global plugins before calling require() on your model file.

    1. const schema = new mongoose.Schema({ name: String });
    2. // Once you `require()` this file, you can no longer add any middleware
    3. // to this schema.
    4. module.exports = mongoose.model('User', schema);

    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. });

    Naming Conflicts

    Mongoose has both query and document hooks for remove().

    1. schema.pre('remove', function() { console.log('Removing!'); });
    2. // Prints "Removing!"
    3. doc.remove();
    4. // Does **not** print "Removing!". Query middleware for `remove` is not
    5. // executed by default.
    6. Model.remove();

    You can pass options to and Schema.post() to switch whether Mongoose calls your remove() hook for or Model.remove(). Note here that you need to set both document and query properties in the passed object:

    1. // Only document middleware
    2. schema.pre('remove', { document: true, query: false }, function() {
    3. console.log('Removing doc!');
    4. });
    5. // Only query middleware. This will get called when you do `Model.remove()`
    6. // but not `doc.remove()`.
    7. schema.pre('remove', { query: true, document: false }, function() {
    8. console.log('Removing!');
    9. });

    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 . Mongoose 4.0 introduced 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. console.log(this instanceof mongoose.Query); // true
    6. // prints returned documents
    7. console.log('find() returned ' + JSON.stringify(result));
    8. // prints number of milliseconds the query took
    9. console.log('find() took ' + (Date.now() - this.start) + ' millis');
    10. });

    For instance, if you wanted to add an updatedAt timestamp to every updateOne() call, you would use the following pre hook.

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

    You cannot access the document being updated in pre('updateOne') or pre('findOneAndUpdate') query middleware. If you need to access the document that will be updated, you need to execute an explicit query for the document.

    However, if you define document middleware, this will be the document being updated. That’s because pre('updateOne') document middleware hooks into Document#updateOne() rather than Query#updateOne().

    1. schema.pre('updateOne', { document: true, query: false }, function() {
    2. console.log('Updating');
    3. });
    4. const Model = mongoose.model('Test', schema);
    5. const doc = new Model();
    6. await doc.updateOne({ $set: { name: 'test' } }); // Prints "Updating"
    7. // Doesn't print "Updating", because `Query#updateOne()` doesn't fire
    8. // document middleware.
    9. await Model.updateOne({}, { $set: { name: 'test' } });

    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 useful for reporting errors and making error messages more readable.

    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. const schema = new Schema({
    2. name: {
    3. type: String,
    4. // Will trigger a MongoServerError 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 === 'MongoServerError' && error.code === 11000) {
    13. next(new Error('There was a duplicate key error'));
    14. } else {
    15. next();
    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 === 'MongoServerError' && error.code === 11000) {
    7. next(new Error('There was a duplicate key error'));
    8. } else {
    9. next(); // The `update()` call will still error out.
    10. }
    11. });
    12. const 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. });

    Error handling middleware can transform an error, but it can’t remove the error. Even if you call next() with no error as shown above, the function call will still error out.

    Aggregation Hooks

    You can also define hooks for the . In aggregation middleware functions, this refers to the Mongoose Aggregate object. For example, suppose you’re implementing soft deletes on a Customer model by adding an isDeleted property. To make sure aggregate() calls only look at customers that aren’t soft deleted, you can use the below middleware to add a to the beginning of each aggregation pipeline.

    1. customerSchema.pre('aggregate', function() {
    2. // Add a $match state to the beginning of each pipeline.
    3. this.pipeline().unshift({ $match: { isDeleted: { $ne: true } } });
    4. });

    The lets you access the MongoDB aggregation pipeline that Mongoose will send to the MongoDB server. It is useful for adding stages to the beginning of the pipeline from middleware.

    Certain Mongoose hooks are synchronous, which means they do not support functions that return promises or receive a next() callback. Currently, only init hooks are synchronous, because the init() function is synchronous. Below is an example of using pre and post init hooks.

    1. const schema = new Schema({ title: String, loadedAt: Date });
    2. schema.pre('init', pojo => {
    3. assert.equal(pojo.constructor.name, 'Object'); // Plain object before init
    4. });
    5. const now = new Date();
    6. schema.post('init', doc => {
    7. assert.ok(doc instanceof mongoose.Document); // Mongoose doc after init
    8. doc.loadedAt = now;
    9. });
    10. const Test = db.model('Test', schema);
    11. return Test.create({ title: 'Casino Royale' }).
    12. then(doc => Test.findById(doc)).
    13. then(doc => assert.equal(doc.loadedAt.valueOf(), now.valueOf()));
    1. const schema = new Schema({ title: String });
    2. const swallowedError = new Error('will not show');
    3. // init hooks do **not** handle async errors or any sort of async behavior
    4. schema.pre('init', () => Promise.reject(swallowedError));
    5. schema.post('init', () => { throw Error('will show'); });
    6. const Test = db.model('Test', schema);
    7. return Test.create({ title: 'Casino Royale' }).
    8. catch(error => assert.equal(error.message, 'will show'));

    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.