/ Programming

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

Mongoose is the most popular MongoDB ORM library to use with Node.js. However, there are a lot of things that can go wrong with the way you use it that took me, and many others that I know, to notice. Here, they will be some useful best-practices I've discovered along the way and some extremely common mistakes made by beginners that I think really should have been addressed in the documentation.

Middleware and validators

I would assume you would be familiar with the middleware and validator features in Mongoose.

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

Beware of validation and middleware bypass

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

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. The problem is that it is not clearly stated 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 rewrite everything!!!

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 the 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 fields 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 middlewares 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 do a call to get the instance 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 to use Mongoose with common mistakes made by beginners 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

Wei-Ming Thor

I am a programmer based in Kuala Lumpur, Malaysia. I write guides on programming and running a software business.

Read More