DEV Community

loading...
Cover image for Mongoose Query Best Practice.

Mongoose Query Best Practice.

Ekekenta Odioyenfe .C
Software Engineer with honed analytical thinking, communication and collaboration skills. Who seeks out innovation solution to everyday problem. Fully dedicate and open to mentorship.
Updated on ・5 min read

MongoDB is undoubtedly one of the most popular NoSQL database choices today. And it has a great community and ecosystem.
In this article, we'll review some of the best practices to follow when you're setting up MongoDB and Mongoose with Node.js.

Pre-requisites for this article

To get the best out of this article, I assume you have some experience with JavaScript (and Node.js) already.

Why do you need Mongoose?

To understand why we need Mongoose, let's understand how MongoDB (and a database) works on the architecture level.

  • You have a database server (MongoDB community server, for example)
  • You have a Node.js script running (as a process)

MongoDB server listens on a TCP socket (usually), and your Node.js process can connect to it using a TCP connection.
But on the top of TCP, MongoDB also has its own protocol for understanding what exactly the client (our Node.js process) wants the database to do.
For this communication, instead of learning the messages we have to send on the TCP layer, we abstract that away with the help of a "driver" software, called MongoDB driver in this case. MongoDB driver is available as an npm package here.
Now remember, the MongoDB driver is responsible for connecting and abstracting the low level communication request/responses from you – but this only gets you so far as a developer.
Because MongoDB is a schemaless database, it gives you way more power than you need as a beginner. More power means more surface area to get things wrong. You need to reduce your surface area of bugs and screw-ups you can make in your code. You need something more.
Meet Mongoose. Mongoose is an abstraction over the native MongoDB driver.

The general rule of thumb with abstractions (the way I understand) is that with every abstraction you lose some low-level operation power. But that doesn't necessarily mean it is bad. Sometimes it boosts productivity 1000x+ because you never really need to have full access to the underlying API anyway.

Similarly, with Mongoose, you can limit your surface area of lower level API access, but unlock a lot of potential gains and good DX.

How to connect MongoDB using Mongoose

mongoose.connect(DB_CONNECTION_STRING, {
        useNewUrlParser: true,
        useUnifiedTopology: true,
        useCreateIndex: true,
        useFindAndModify: false
})
Enter fullscreen mode Exit fullscreen mode

The first thing you must do when using Mongoose and Mongodb is to create a database connection. Doing this the right is one of the basic things one must do.
The above connection format makes sure that you're using the new URL Parser from Mongoose, and that you are not using any deprecated practices. You can read in depth about all these deprecation messages here if you like.

How to use Mongoose Models

Models are the superpower of Mongoose. They help you enforce "schema" rules and provide a seamless integration of your Node code into database calls.
The very first step is to define a good model:

const mongoose = require("mongoose");

const userSchema = new mongoose.Schema(
        {
                username : { type : String, required : true}
                dob : { type: Date, required: true },
                location : { type: String, required: true },
                hashedPassword : { type: String, required: true }
        },
        { collection: 'users' }
)
const User = mongoose.model('Users', userSchema)
module.exports = User 
Enter fullscreen mode Exit fullscreen mode

There are few interesting things you should note here:

  1. Try to keep required: true on all fields which are required. This can be a huge pain saver for you if you don't use a static type checking system like TypeScript to assist you with correct property names while creating an object. Plus the free validation is super cool, too.
  2. Define a collection name explicitly. Although Mongoose can automatically give a collection name based on the name of model (Completed here, for example), this is way too much abstraction in my opinion. You should at least know about your database names and collections in your codebase.
  3. Define indexes and unique fields. unique property can also be added within a schema. Indexes are a broad topic, so I will not go into depth here. But on a large scale they can really help you to speed up your queries a lot.
  4. Always make sure that user passwords are not stored as raw strings in the database, the used hashed to reduce its vulnarability.

How to perform Mongoose operations

Let's now go ahead and quickly discuss operations with Mongoose, and how you should perform them.
Let perform some operations in mongoose. We will be using an POST API have created the here.
When a get request is send to an API lets say to get the list of registered users. Some manay developer would do something like or similar to this.

// lets use our User model
const models = require("./models/usersModel");
app.get('/api/Users', (req,res)=>{
    models.User.find({}, (err, users)=>{
     if(err) throw err;
    })
     res.json(users)  
})
Enter fullscreen mode Exit fullscreen mode

The above code runs successfully, but thus vulnerable. Sensitive data like the user password are been sent to the client which they have no use for, but made available to attackers.

To perform the same operation in a safer way you first of all know the data would be used by the client side and send them.

// lets say we want to display the username, location and date of birth
const models = require("./models/usersModel");
app.get('/api/Users', async (req,res)=>{
    try{
      const users = await models.User.find({}, ['username', 'location', 'dob'])
        .lean()
        .exec();
        res.json(users)
    catch(err){}
)
Enter fullscreen mode Exit fullscreen mode

In the above query operation there couple of things you should know with Mongoose.

  1. Can you see the lean() function call there? It is super useful for performance. By default, Mongoose processes the returned document(s) from the database and adds its magical methods on it (for example .save)
  2. When you use .lean(), Mongoose returns plain JSON objects instead of memory and resource heavy documents. Makes queries faster and less expensive on your CPU, too.
  3. However, you can omit .lean() if you are actually thinking of updating data (we'll see that next)
  4. Mongoose will not execute a query until then or exec has been called upon it. This is very useful when building complex queries.
  5. Use async-await instead of callbacks (nice on the eyes, no ground breaking performance benefit as such)
  6. Use try-catch blocks around queries because your query can fail for a number of reasons (duplicate record, incorrect value, and so on)

The Update Operation

This time around we are not calling the .lean() function, we can simply go ahead and modify the object property, and save it using object.save():

const doc = await User.findOne(info)
doc.loacation= 'some where differnet'
await doc.save()
Enter fullscreen mode Exit fullscreen mode

Remember that here, there are two database calls made. The first one is on findOne and the second one is on doc.save.
It is advisable, you should always reduce the number of requests going to the database (because if you're comparing memory, network, and disk, network is almost always the slowest).
In the other case, you can use a query like this:

const res = await User.updateOne({username:"user's username":}, {location:"some where different "}).lean()
Enter fullscreen mode Exit fullscreen mode

and it will only make a single call to the database.

The Delete Operation

Delete is also straightforward with Mongoose. Let's see how you can delete a single document:

const res = await User.deleteOne({username:"user's username"})
Enter fullscreen mode Exit fullscreen mode

Just like the updateOne, deleteOne also accepts the first argument as the matching condition for the document.
There is also another method called deleteMany which should be used only when you know you want to delete multiple documents.
In any other case, always use deleteOne to avoid accidental multiple deletes, especially when you're trying to execute queries yourself.

Conclusion

This article was to introduce you to some of best practices you need to use when performing CRUD operations using Mongodb and Mongoose.
To know more, I recommend you to also read the Mongodb documentation and Mongoose documentation increase mastery.

Discussion (0)