Pre and post middleware hooks is a very useful feature in Mongoose and provides a lot of flexibility during database operations like query, create, remove etc. Pre and post hooks are functions that are executed before or after a particular action that you specify. These hooks get triggered whenever you perform an operation with your database.

Types of Middleware

Mongoose has 4 types of middleware: document middleware, model middleware, aggregate middleware, and query middleware.

DocumentQueryAggregateModel
validatecountaggregateinsertMany
savedeleteMany
removedeleteOne
updateOnefind
deleteOnefindOne
init*findOneAndDelete
findOneAndRemove
findOneAndUpdate
remove
update
updateOne
updateMany

*init hooks are synchronous

Note: If you specify schema.pre('remove'), Mongoose will register this middleware for doc.remove() by default. If you want to your middleware to run on Query.remove() use schema.pre('remove', { query: true, document: false }, fn).

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.

Defining middlewares

  • use normal callback with next parameter

The next parameter is a function provided to you by mongoose to have a way out, or to tell mongoose you are done and to continue with the next step in the execution chain. Also it is possible to pass an Error to next it will stop the execution chain.

1
2
3
4
5
6
schema.pre('save', function(next) {
  // do stuff
  
  if (error) { return next(new Error("something went wrong"); }
  return next(null);
});
  • use async callback

Here the execution chain will continue once your async callback has finished. If there is an error and you want to break/stop execution chain you just throw it

1
2
3
4
5
6
7
8
schema.pre('save', async function() {
  // do stuff
  await doStuff()
  await doMoreStuff()

  if (error) { throw new Error("something went wrong"); }
  return;
});

Note 1:- You must add all middleware and plugins before calling mongoose.model().

Note 2:- Remember we cannot use arrow function inside pre hooks as arrow function are poor binding functions.

Understanding Middlewares with an example

Let us define a User schema as below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const userSchema = new mongoose.Schema({
  _id      : { type: String },
  name     : { type: String },
  username : { type: String },
  password : { type: String }
});


userSchema.pre('save', async function() {
  console.log('pre hook: validate username');
  const user = this;
  const regex = /^[a-zA-Z0-9]+$/;
  const result = regex.test(user.username);

  if (result) {
    console.log('username validated');

    return Promise.resolve();
  } else {
    console.log('this will stop execution of next middlewares');
    throw new Error('username is not alphanumeric');
  }
});

userSchema.pre('save', async function() {
  console.log('pre hook: generate password hash');
  const user = this;

  const passHash = await getSHAHash(user.password);
  console.log('password hash generated');
  user.password = passHash;
});


userSchema.post('save', async doc => {
  console.log('post hook called');
  console.log('notify/send email');
});

const User = mongoose.model('User', userSchema);

userSchema.pre('save', async () => {
  console.log('pre hook: after calling mongoose.model()');
  console.log('this pre hook will not be called');
});

In the above schema;

  • a pre hook to validate whether the provided username contains only alphanumberic characters
  • a pre hook to generate hash of the user password
  • a post hook to notify/send email
  • a pre hook added post mongosse.model()

Run the below code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const init = async () => {
  try {
    console.log('start of the script');
    await User.create({ _id: Date.now(), name: 'Manish', username: 'manisuec', password: 'abc123' });
    console.log('first user created successfully');
    console.log('-------------------------------------------');
    await User.create({ _id: Date.now(), name: 'Ravi', username: 'ravi@123', password: 'abc123' });
  } catch (err) {
    console.log('error:', err.message);
  }
}

init();

You should see output as below and I guess the output is self-explanatory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
pre hook: validate username
username validated

pre hook: generate password hash
password hash generated

post hook called
notify/send email
first user created successfully
-------------------------------------------
pre hook: validate username
this will stop execution of next middlewares
error: username is not alphanumeric

The sample code is checked into mongoose-hooks on github.

Use Cases

Pre-hooks can be utilized in a variety of scenarios, such as custom validations, generating hash password etc. You can also use them when you have a field named say "archived" in your schema and you wish to exclude all archived documents in each "find" call. Including a filter for "archived: false" in every "find" call may be cumbersome and error-prone. In such cases, pre-hooks can serve as query middleware to simplify the process. For instance, a simple pre-hook on the "find" method in Mongoose can modify the query and add an extra filter, which will be applied to all subsequent "find" queries for that model.

Another use case can be to add additional field say last_updatedMS by default which saves epoch timestamp value on every update operation.

Hooks have a multitude of use cases depending on your application and I hope you can identify potential use cases for your own application.

Conclusion

I hope that in this article, you have gained an understanding of how and when to utilize pre and post hooks in mongoose, as well as the benefits they provide in comparison to writing repetitive logic for actions performed on collection documents.

By implementing these hooks, we can reduce the amount of code we need to write, resulting in a smaller potential for bugs. Additionally, we can alleviate the mental burden of having to remember to execute certain logic before or after a method each time. There are various other use cases for pre and post middleware hooks, and their execution before the method they are hooked onto makes them a versatile tool.

✨ Thank you for reading and I hope you find it helpful. I sincerely request for your feedback in the comment’s section.