API with NestJS #44. Implementing relationships with MongoDB

JavaScript MongoDB NestJS

This entry is part 44 of 48 in the API with NestJS

An essential thing about MongoDB is that it is non-relational. Therefore, it might not be the best fit if relationships are a big part of our database design. That being said, we definitely can mimic SQL-style relations by using references of embedding documents directly.

You can get all of the code from this article in this repository.

Defining the initial schema

In this article, we base the code on many of the functionalities we’ve implemented in the previous parts of this series. If you want to know how we register and authenticate users, check out API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies.

Let’s start by defining a schema for our users.

user.schema.ts

A few significant things are happening above. We use above to make sure that all users have unique emails. It sets up unique indexes under the hood and deserves a separate article.

The  and decorators come from the library. We cover serialization in more detail in API with NestJS #5. Serializing the response with interceptors. There is a significant catch here with MongoDB and Mongoose, though.

The Mongoose library that we use for connecting to MongoDB and fetching entities does not return instances of our class. Therefore, the won’t work out of the box. Let’s change it a bit using the mixin pattern.

mongooseClassSerializer.interceptor.ts

I wrote the above code with the help of Jay McDoniel. The official NestJS discord is a great place to ask for tips.

Above, we change MongoDB documents into instances of the provided class. Let’s use it with our controller:

authentication.controller.ts

Thanks to doing the above, we exclude the password when returning the data of the user.

One-To-One

With the one-to-one relationship, the document in the first collection has just one matching document in the second collection and vice versa. Let’s create a schema for the address:

address.schema.ts

There is a big chance that just one user is assigned to a particular address in our application. Therefore, it is a good example of a one-to-one relationship. Because of that, we can take advantage of embedding documents, which is an approach very good performance-wise.

For it to work properly, we need to explicitly pass to the decorator:

user.schema.ts

We use above to make sure that the transforms the object too.

When we create the document for the user, MongoDB also creates the document for the address. It also gives it a distinct id.

In our one-to-one relationship example, the user has just one address. Also, one address belongs to only one user. Since that’s the case, it makes sense to embed the user straight into the user’s document. This way, MongoDB can return it fast. Let’s use MongoDB Compass to make sure that this is the case here.

One-To-Many

We implement the one-to-many and many-to-one relationships when a document from the first collection can be linked to multiple documents from the second collection. Documents from the second collection can be linked to just one document from the first collection.

Great examples are posts and authors where the user can be an author of multiple posts. In our implementation, the post can only have one author, though.

post.schema.ts

Thanks to defining the above reference, we can now assign the user to the property in the post.

posts.service.ts

Populating the data with Mongoose

Saving the posts like that results in storing the id of the author in the database.

 

A great thing about it is that we can easily replace the id with the actual data using the function Mongoose provides.

posts.service.ts

Doing the above results in Mongoose returning the data of the author along with the post.

The direction of the reference

In the code above, we store the id of the author in the document of the post. We could do that the other way around and store the posts’ id in the author’s document. When deciding that, we need to take a few factors into account.

First, we need to think of how many references we want to store. Imagine a situation where we want to store logs for different machines in our server room. We need to remember that the maximum size of a MongoDB document is 16MB. If we store an array of the ids of the document in the document, in theory, we could run out of space at some point. We can store a single id of the machine in the document instead.

The other thing to think through is what queries we will run most often. For example, in our implementation of posts and authors, it is effortless to retrieve the author’s data if we have the post. This is thanks to the fact that we store the author’s id in the document of the post. On the other hand, it would be more time-consuming to retrieve a list of posts by a single user. To do that, we would need to query all of the posts and check the author’s id.

We could implement two-way referencing and store the reference on both sides to deal with the above issue. The above would speed up some of the queries but require us to put more effort into keeping our data consistent.

Embedding

We could also embed the document of the posts into the document of the user. The advantage of doing that would be not performing additional queries to the database to get the missing information. But, unfortunately, this would make getting a particular post more difficult.

Many-to-many

Another important relationship to consider is many-to-many. A document from the first collection can refer to multiple documents from the second collection and the other way around.

A good example would be posts that can belong to multiple categories. Also, a single category can belong to multiple posts. First, let’s define the schema of our category.

category.schema.ts

Now we can use it in the schema of the user.

user.schema.ts

A thing worth knowing is that we can also use the method right after saving our document.

An important thing above is that we call the method on an instance of a MongoDB document. Since that’s the case, we also need to call for it to run. This is not needed in the rest of our examples where we call on an instance of the MongoDB query.

Summary

In this article, we’ve covered defining relationships between documents in MongoDB using NestJS. We’ve learned various types of relationships and considered how to store the references to increase the performance. We’ve also touched on the subject of how to implement serialization with NestJS and MongoDB. There is still quite a lot to learn, so stay tuned!

Series Navigation<< API with NestJS #43. Introduction to MongoDBAPI with NestJS #45. Virtual properties with MongoDB and Mongoose >>
Subscribe
Notify of
guest
4 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
ola
ola
1 month ago

I love this series thanks dude, but how can i apply this to suit my code base i use postgres, sequelize, express and nodejs, is this possible to suit my code? i am reffering to the series that you use postgress, typeorm nestjs

Last edited 1 month ago by ola
Chris
Chris
27 days ago
Reply to  Marcin Wanago

Hi! I’m too excited with this serie-course!!! Please! Keep it up! Will you add more topics?