API with NestJS #44. Implementing relationships with MongoDB

JavaScript MongoDB NestJS

This entry is part 44 of 168 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
18 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
ola
ola
3 years 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 3 years ago by ola
Chris
Chris
3 years ago
Reply to  Marcin Wanago

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

Ryan Nguyen
Ryan Nguyen
2 years ago
Reply to  Marcin Wanago

Your series are fantastic!

Juan González
Juan González
2 years ago

Hello,

I have in my code Rol.schema

@Prop({
    type: { type: mongoose.Schema.Types.ObjectId, ref: Permission.name },
  })
  @Type(() => Permission)
  permissions: Permission;

Doing:
await createdRol.populate(‘permissions’).execPopulate();

Error:

Property ‘execPopulate’ does not exist on type ‘Promise<Rol & Document<any, any, any> & { _id: any; }>’.

Thanks for your answer.

John
John
2 years ago
Reply to  Juan González

Hello, execPopulate is no longer available so you need just to use
await createdRol.populate(‘permissions’);

Seghosimhe David
Seghosimhe David
2 years ago

Hello Marcin Wanago,
Please How can I get to see your DTO for the register user

Gürkan
2 years ago

Love your posts so much. This series taught me many things about backend development!

Adrian Gonzalez
2 years ago

I have the following error:

My code here is:

And for user:

Someone know what’s wrong? Any suggestion?
(imports are all correctly imported)

Illia
Illia
1 year ago

Hi, I have a problem with MongooseClassSerializerInterseptor. It works as intended for class-transformer but it also mutates _id on the output. I get different _id for the same entity every time on client.

My user.schema.ts

Fapalz
Fapalz
1 year ago
Reply to  Illia

I have the same problem! did you solve it?

Ben
Ben
1 year ago
Reply to  Illia

I had the same issue. This worked for me:

On the schema, instead of this:

Do this:

YEE
YEE
1 year ago

I love u!!!!

Ashish
Ashish
1 year ago

Please share the solution for typeORM with mongoDB to implement many to many bidirectional relation in NEST
I am getting this error – TypeError: Cannot read properties of undefined (reading ‘_id’)I have used many-to-many bi-directional relation in employees and meetings entity, firstly I added meeting link and then added attendees as array , the meeting is getting saved but the above error is coming. (Without adding the attendees, no such error comes).I have been looking into this for so long

Ahmad
Ahmad
1 year ago
Reply to  Ashish