Best practices and common beginner mistakes for Mongoose in Node.js

Mongoose is the most popular MongoDB ORM library to use with Node.js. Here, there will be some useful best practices I've discovered along the way and some extremely common mistakes made when new to the ORM.

Validators and Middlewares

One issue that you might come across is the middleware and validator not being called. The danger of this is that you might unknowingly write invalid data into your database, bypassing your model checks entirely.

If you're not too familiar with middleware and validators in Mongoose, here is a quick brief and some examples.

Validators

They ensure the integrity of the data that you write into MongoDB.

var userSchema = new Schema({
    age: {
        type: Number,
        min: [13, 'Too young'],
        max: 130,
    },
});

Middlewares

Middlewares are functions that you can define within your Mongoose models to be executed to modify data either before or after saving.

userSchema.pre('save', function capitalizeNames(next) {
  this.firstName = capitalize(this.firstName);
  this.lastName = capitalize(this.lastName);
  next();
});

The reason this happens is that Mongoose provides both the features to handle data using the ORM and also features to write directly into the database. However, they are not clearly stated and differentiated in the documentation*.

* with the exception of some tiny footnotes

Just to give an example, if you are using any of these function, then you unknowingly facing this issue and should probably look into using the right method calls.

User.create();

User.findOneAndUpdate();
User.findByIdAndUpdate();

User.remove();
User.findOneAndRemove();
User.findByIdAndRemove();

Queries and ORM

Queries

The reason you shouldn't use any of these (unless you are aware of the use cases for them, e.g Unit tests), is because they are calls to make direct queries to the MongoDB database. They are not modifications to an instance of the data wrapped in the Mongoose ORM.

// Direct query. Beware!
User.create();

ORM

The following is an example of modifying an instance, which is how you would know that your data passes through the ORM.

// You most probably want this
User user = new User({
    name: 'Brian',
});
user.create();

The difference is simply that this creates a model instance of the data and you make the modification to it.

CRUD

Below are examples of the best ways to make Create, Read, Update, Delete (CRUD) queries that go through the validators and middlewares.

Create

To create you would want to create an instance of the object and then save to validate through the model rather than write directly to MongoDB using the following method. It is essentially the same as the example above.

const user = new User({
    name: req.body.name,
    age: req.body.age,
    country: req.body.country
});

user.save((saveErr, savedUser) => {
    res.status(201).send({ data: savedUser });
});

The following writes directly to MongoDB and should usually be avoided unless certain cases like writing tests or other cases when you deliberately want to write in invalid data.

// Avoid
User.create(req.body, (createErr, createdUser) => {
    res.status(201).send({ data: createdUser });
});

Read

There are usually no issues with reading data as you are making a query to receive the data instance. Any of these calls would work fine.

User.findOne();
User.findById();

Update

Depending on how you perform an update, there are several optimized ways that I have found to do them. There is the PUT and PATCH-based update. Some frameworks you may have used such as Ruby on Rails treat them the same though there is a technical difference between them.

PUT

In a PUT request, you pass the entire object in the body of the request. It would contain all the necessary fields to update the object.

User.findById(req.params.id, (err, user) => {
    // This assumes all the fields of the object is present in the body.
    user.name = req.body.name;
    user.age = req.body.age;
    user.country = req.body.country;
    
    user.save((saveErr, updatedUser) => {
        res.send({ data: updatedUser });
    });
});

PATCH

In a PATCH request, you would pass only the fields that you want to update. Therefore, not a field will be present in the body as above. It's a slightly different method of working with it but the following would also work for the case above, and is a cleaner method.

User.findById(req.params.id, (err, user) => {
    // Update user with the available fields
    // This assumes the field name is the same in the form and the database.
    user.set(req.body);
    
    user.save((saveErr, updatedUser) => {
        res.send({ data: updatedUser });
    });
);

Direct writes

The following updates directly into MongoDB and you want to avoid them unless you want to skip the middleware and field validations.

// Avoid
User.findByIdAndUpdate();
User.findOneAndUpdate();

Delete

When deleting an object, validation is not as critical but you want to make sure that the middlewares are called if you have any. For example, when the object has dependencies, you would have middlewares to delete them or do some other cleanup. To ensure that they are called, you would make a call to get the instance and then delete it.

User.findById(req.params.id, (err, user) => {
    user.remove((userErr, removedUser) => {
        res.send({ data: removedUser });
    });
});

These other alternatives directly remove from MongoDB and bypass the middleware.

// Avoid
User.remove();
User.findByIdAndRemove();
User.findOneAndRemove();

Model security

You would also want to implement the select parameter to prevent selecting large objects that can also contain sensitive information. Ensure to set sensitive fields to not be selected by default, therefore require them to be explicitly selected.

const userSchema = new mongoose.Schema({
    firstName: {
        type: String,
    },
    email: {
        type: String,
        select: false, // No select
    },
    password: {
        type: String,
        select: false, // No select
    },
});

/* ... */

// Explicit select
User.findById(req.params.id).select('+email').exec((err, user) => {
    res.send({ data: user });
});

Conclusion

This is a basic guide on how to use Mongoose with common mistakes and some best practices. Essentially you want to ensure that you always make calls that go through middlewares and validators and avoid direct writes unless for specific cases.

Wei-Ming Thor

I write guides on Software Engineering, Data Science, and Machine Learning.

Background

Full-stack engineer who builds web and mobile apps. Now, exploring Machine Learning and Data Engineering.

Writing unmaintainable code since 2010.

Skill/languages

Best: JavaScript, Python
Others: Android, iOS, C, React Native, Ruby, PHP

Work

Engineering Manager

Location

Kuala Lumpur, Malaysia

Open Source
Support

Turn coffee into coding guides. Buy me coffee