- 1. API with NestJS #1. Controllers, routing and the module structure
- 2. API with NestJS #2. Setting up a PostgreSQL database with TypeORM
- 3. API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
- 4. API with NestJS #4. Error handling and data validation
- 5. API with NestJS #5. Serializing the response with interceptors
- 6. API with NestJS #6. Looking into dependency injection and modules
- 7. API with NestJS #7. Creating relationships with Postgres and TypeORM
- 8. API with NestJS #8. Writing unit tests
- 9. API with NestJS #9. Testing services and controllers with integration tests
- 10. API with NestJS #10. Uploading public files to Amazon S3
- 11. API with NestJS #11. Managing private files with Amazon S3
- 12. API with NestJS #12. Introduction to Elasticsearch
- 13. API with NestJS #13. Implementing refresh tokens using JWT
- 14. API with NestJS #14. Improving performance of our Postgres database with indexes
- 15. API with NestJS #15. Defining transactions with PostgreSQL and TypeORM
- 16. API with NestJS #16. Using the array data type with PostgreSQL and TypeORM
- 17. API with NestJS #17. Offset and keyset pagination with PostgreSQL and TypeORM
- 18. API with NestJS #18. Exploring the idea of microservices
- 19. API with NestJS #19. Using RabbitMQ to communicate with microservices
- 20. API with NestJS #20. Communicating with microservices using the gRPC framework
- 21. API with NestJS #21. An introduction to CQRS
- 22. API with NestJS #22. Storing JSON with PostgreSQL and TypeORM
- 23. API with NestJS #23. Implementing in-memory cache to increase the performance
- 24. API with NestJS #24. Cache with Redis. Running the app in a Node.js cluster
- 25. API with NestJS #25. Sending scheduled emails with cron and Nodemailer
- 26. API with NestJS #26. Real-time chat with WebSockets
- 27. API with NestJS #27. Introduction to GraphQL. Queries, mutations, and authentication
- 28. API with NestJS #28. Dealing in the N + 1 problem in GraphQL
- 29. API with NestJS #29. Real-time updates with GraphQL subscriptions
- 30. API with NestJS #30. Scalar types in GraphQL
- 31. API with NestJS #31. Two-factor authentication
- 32. API with NestJS #32. Introduction to Prisma with PostgreSQL
- 33. API with NestJS #33. Managing PostgreSQL relationships with Prisma
- 34. API with NestJS #34. Handling CPU-intensive tasks with queues
- 35. API with NestJS #35. Using server-side sessions instead of JSON Web Tokens
- 36. API with NestJS #36. Introduction to Stripe with React
- 37. API with NestJS #37. Using Stripe to save credit cards for future use
- 38. API with NestJS #38. Setting up recurring payments via subscriptions with Stripe
- 39. API with NestJS #39. Reacting to Stripe events with webhooks
- 40. API with NestJS #40. Confirming the email address
- 41. API with NestJS #41. Verifying phone numbers and sending SMS messages with Twilio
- 42. API with NestJS #42. Authenticating users with Google
- 43. API with NestJS #43. Introduction to MongoDB
- 44. API with NestJS #44. Implementing relationships with MongoDB
- 45. API with NestJS #45. Virtual properties with MongoDB and Mongoose
- 46. API with NestJS #46. Managing transactions with MongoDB and Mongoose
- 47. API with NestJS #47. Implementing pagination with MongoDB and Mongoose
- 48. API with NestJS #48. Definining indexes with MongoDB and Mongoose
- 49. API with NestJS #49. Updating with PUT and PATCH with MongoDB and Mongoose
- 50. API with NestJS #50. Introduction to logging with the built-in logger and TypeORM
- 51. API with NestJS #51. Health checks with Terminus and Datadog
- 52. API with NestJS #52. Generating documentation with Compodoc and JSDoc
- 53. API with NestJS #53. Implementing soft deletes with PostgreSQL and TypeORM
- 54. API with NestJS #54. Storing files inside a PostgreSQL database
- 55. API with NestJS #55. Uploading files to the server
- 56. API with NestJS #56. Authorization with roles and claims
- 57. API with NestJS #57. Composing classes with the mixin pattern
- 58. API with NestJS #58. Using ETag to implement cache and save bandwidth
- 59. API with NestJS #59. Introduction to a monorepo with Lerna and Yarn workspaces
- 60. API with NestJS #60. The OpenAPI specification and Swagger
- 61. API with NestJS #61. Dealing with circular dependencies
- 62. API with NestJS #62. Introduction to MikroORM with PostgreSQL
- 63. API with NestJS #63. Relationships with PostgreSQL and MikroORM
- 64. API with NestJS #64. Transactions with PostgreSQL and MikroORM
- 65. API with NestJS #65. Implementing soft deletes using MikroORM and filters
- 66. API with NestJS #66. Improving PostgreSQL performance with indexes using MikroORM
- 67. API with NestJS #67. Migrating to TypeORM 0.3
- 68. API with NestJS #68. Interacting with the application through REPL
- 69. API with NestJS #69. Database migrations with TypeORM
- 70. API with NestJS #70. Defining dynamic modules
- 71. API with NestJS #71. Introduction to feature flags
- 72. API with NestJS #72. Working with PostgreSQL using raw SQL queries
- 73. API with NestJS #73. One-to-one relationships with raw SQL queries
- 74. API with NestJS #74. Designing many-to-one relationships using raw SQL queries
- 75. API with NestJS #75. Many-to-many relationships using raw SQL queries
- 76. API with NestJS #76. Working with transactions using raw SQL queries
- 77. API with NestJS #77. Offset and keyset pagination with raw SQL queries
- 78. API with NestJS #78. Generating statistics using aggregate functions in raw SQL
- 79. API with NestJS #79. Implementing searching with pattern matching and raw SQL
- 80. API with NestJS #80. Updating entities with PUT and PATCH using raw SQL queries
- 81. API with NestJS #81. Soft deletes with raw SQL queries
- 82. API with NestJS #82. Introduction to indexes with raw SQL queries
- 83. API with NestJS #83. Text search with tsvector and raw SQL
- 84. API with NestJS #84. Implementing filtering using subqueries with raw SQL
- 85. API with NestJS #85. Defining constraints with raw SQL
- 86. API with NestJS #86. Logging with the built-in logger when using raw SQL
- 87. API with NestJS #87. Writing unit tests in a project with raw SQL
- 88. API with NestJS #88. Testing a project with raw SQL using integration tests
- 89. API with NestJS #89. Replacing Express with Fastify
- 90. API with NestJS #90. Using various types of SQL joins
- 91. API with NestJS #91. Dockerizing a NestJS API with Docker Compose
- 92. API with NestJS #92. Increasing the developer experience with Docker Compose
- 93. API with NestJS #93. Deploying a NestJS app with Amazon ECS and RDS
- 94. API with NestJS #94. Deploying multiple instances on AWS with a load balancer
- 95. API with NestJS #95. CI/CD with Amazon ECS and GitHub Actions
- 96. API with NestJS #96. Running unit tests with CI/CD and GitHub Actions
- 97. API with NestJS #97. Introduction to managing logs with Amazon CloudWatch
- 98. API with NestJS #98. Health checks with Terminus and Amazon ECS
- 99. API with NestJS #99. Scaling the number of application instances with Amazon ECS
- 100. API with NestJS #100. The HTTPS protocol with Route 53 and AWS Certificate Manager
- 101. API with NestJS #101. Managing sensitive data using the AWS Secrets Manager
- 102. API with NestJS #102. Writing unit tests with Prisma
- 103. API with NestJS #103. Integration tests with Prisma
- 104. API with NestJS #104. Writing transactions with Prisma
- 105. API with NestJS #105. Implementing soft deletes with Prisma and middleware
- 106. API with NestJS #106. Improving performance through indexes with Prisma
- 107. API with NestJS #107. Offset and keyset pagination with Prisma
- 108. API with NestJS #108. Date and time with Prisma and PostgreSQL
- 109. API with NestJS #109. Arrays with PostgreSQL and Prisma
- 110. API with NestJS #110. Managing JSON data with PostgreSQL and Prisma
- 111. API with NestJS #111. Constraints with PostgreSQL and Prisma
- 112. API with NestJS #112. Serializing the response with Prisma
- 113. API with NestJS #113. Logging with Prisma
- 114. API with NestJS #114. Modifying data using PUT and PATCH methods with Prisma
- 115. API with NestJS #115. Database migrations with Prisma
- 116. API with NestJS #116. REST API versioning
- 117. API with NestJS #117. CORS – Cross-Origin Resource Sharing
- 118. API with NestJS #118. Uploading and streaming videos
- 119. API with NestJS #119. Type-safe SQL queries with Kysely and PostgreSQL
- 120. API with NestJS #120. One-to-one relationships with the Kysely query builder
- 121. API with NestJS #121. Many-to-one relationships with PostgreSQL and Kysely
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; import { Exclude, Transform } from 'class-transformer'; export type UserDocument = User & Document; @Schema() export class User { @Transform(({ value }) => value.toString()) _id: string; @Prop({ unique: true }) email: string; @Prop() name: string; @Prop() @Exclude() password: string; } export const UserSchema = SchemaFactory.createForClass(User); |
A few significant things are happening above. We use unique: true above to make sure that all users have unique emails. It sets up unique indexes under the hood and deserves a separate article.
The @Exclude and @Transform decorators come from the class-transformer 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 User class. Therefore, the ClassSerializerInterceptor won’t work out of the box. Let’s change it a bit using the mixin pattern.
mongooseClassSerializer.interceptor.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import { ClassSerializerInterceptor, PlainLiteralObject, Type, } from '@nestjs/common'; import { ClassTransformOptions, plainToClass } from 'class-transformer'; import { Document } from 'mongoose'; function MongooseClassSerializerInterceptor( classToIntercept: Type, ): typeof ClassSerializerInterceptor { return class Interceptor extends ClassSerializerInterceptor { private changePlainObjectToClass(document: PlainLiteralObject) { if (!(document instanceof Document)) { return document; } return plainToClass(classToIntercept, document.toJSON()); } private prepareResponse( response: PlainLiteralObject | PlainLiteralObject[], ) { if (Array.isArray(response)) { return response.map(this.changePlainObjectToClass); } return this.changePlainObjectToClass(response); } serialize( response: PlainLiteralObject | PlainLiteralObject[], options: ClassTransformOptions, ) { return super.serialize(this.prepareResponse(response), options); } }; } export default MongooseClassSerializerInterceptor; |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import { Body, Controller, Post, UseInterceptors, } from '@nestjs/common'; import { AuthenticationService } from './authentication.service'; import RegisterDto from './dto/register.dto'; import { User } from '../users/user.schema'; import MongooseClassSerializerInterceptor from '../utils/mongooseClassSerializer.interceptor'; @Controller('authentication') @UseInterceptors(MongooseClassSerializerInterceptor(User)) export class AuthenticationController { constructor(private readonly authenticationService: AuthenticationService) {} @Post('register') async register(@Body() registrationData: RegisterDto) { return this.authenticationService.register(registrationData); } // ... } |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; import { Transform } from 'class-transformer'; export type AddressDocument = Address & Document; @Schema() export class Address { @Transform(({ value }) => value.toString()) _id: string; @Prop() city: string; @Prop() street: string; } export const AddressSchema = SchemaFactory.createForClass(Address); |
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 AddressSchema to the @Prop decorator:
user.schema.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document, ObjectId } from 'mongoose'; import { Exclude, Transform, Type } from 'class-transformer'; import { Address, AddressSchema } from './address.schema'; export type UserDocument = User & Document; @Schema() export class User { @Transform(({ value }) => value.toString()) _id: ObjectId; @Prop({ unique: true }) email: string; @Prop() name: string; @Prop() @Exclude() password: string; @Prop({ type: AddressSchema }) @Type(() => Address) address: Address; } export const UserSchema = SchemaFactory.createForClass(User); |
We use @Type(() => Address) above to make sure that the class-transformer transforms the Address 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document, ObjectId } from 'mongoose'; import * as mongoose from 'mongoose'; import { User } from '../users/user.schema'; import { Transform, Type } from 'class-transformer'; export type PostDocument = Post & Document; @Schema() export class Post { @Transform(({ value }) => value.toString()) _id: ObjectId; @Prop() title: string; @Prop() content: string; @Prop({ type: mongoose.Schema.Types.ObjectId, ref: User.name }) @Type(() => User) author: User; } export const PostSchema = SchemaFactory.createForClass(Post); |
Thanks to defining the above reference, we can now assign the user to the author property in the post.
posts.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import { Model } from 'mongoose'; import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Post, PostDocument } from './post.schema'; import PostDto from './dto/post.dto'; import { User } from '../users/user.schema'; @Injectable() class PostsService { constructor(@InjectModel(Post.name) private postModel: Model<PostDocument>) {} create(postData: PostDto, author: User) { const createdPost = new this.postModel({ ...postData, author, }); return createdPost.save(); } // ... } export default PostsService; |
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 populate function Mongoose provides.
posts.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import { Model } from 'mongoose'; import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Post, PostDocument } from './post.schema'; @Injectable() class PostsService { constructor(@InjectModel(Post.name) private postModel: Model<PostDocument>) {} async findAll() { return this.postModel.find().populate('author'); } // ... } export default PostsService; |
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 Log document in the Machine document, in theory, we could run out of space at some point. We can store a single id of the machine in the Log 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document, ObjectId } from 'mongoose'; import { Transform } from 'class-transformer'; export type CategoryDocument = Category & Document; @Schema() export class Category { @Transform(({ value }) => value.toString()) _id: ObjectId; @Prop() name: string; } export const CategorySchema = SchemaFactory.createForClass(Category); |
Now we can use it in the schema of the user.
user.schema.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document, ObjectId } from 'mongoose'; import * as mongoose from 'mongoose'; import { User } from '../users/user.schema'; import { Transform, Type } from 'class-transformer'; import { Category } from '../categories/category.schema'; export type PostDocument = Post & Document; @Schema() export class Post { @Transform(({ value }) => value.toString()) _id: ObjectId; @Prop() title: string; @Prop() content: string; @Prop({ type: mongoose.Schema.Types.ObjectId, ref: User.name }) @Type(() => User) author: User; @Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: Category.name }], }) @Type(() => Category) categories: Category; } export const PostSchema = SchemaFactory.createForClass(Post); |
A thing worth knowing is that we can also use the populate method right after saving our document.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import { Model } from 'mongoose'; import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Post, PostDocument } from './post.schema'; import PostDto from './dto/post.dto'; import { User } from '../users/user.schema'; @Injectable() class PostsService { constructor(@InjectModel(Post.name) private postModel: Model<PostDocument>) {} async create(postData: PostDto, author: User) { const createdPost = new this.postModel({ ...postData, author, }); await createdPost.populate('categories').execPopulate(); return createdPost.save(); } // ... } export default PostsService; |
An important thing above is that we call the populate method on an instance of a MongoDB document. Since that’s the case, we also need to call execPopulate for it to run. This is not needed in the rest of our examples where we call populate 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!
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
If you don’t use NestJS in your code but use Express, you might be interested in my TypeScript Express series.
Hi! I’m too excited with this serie-course!!! Please! Keep it up! Will you add more topics?
Thank you. I’ve just published a new article in this series 🙂
Your series are fantastic!
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.
Hello, execPopulate is no longer available so you need just to use
await createdRol.populate(‘permissions’);
Hello Marcin Wanago,
Please How can I get to see your DTO for the register user
You can find all of the code from this article here:
https://github.com/mwanago/nestjs-mongodb
Love your posts so much. This series taught me many things about backend development!
I have the following error:
My code here is:
And for user:
Someone know what’s wrong? Any suggestion?
(imports are all correctly imported)
Hi. Looks like you have a circular dependency in your imports. The “User” being undefined is what might happen in a case like that.
https://dev.wanago.io/2022/02/28/api-nestjs-circular-dependencies/
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
I have the same problem! did you solve it?
I had the same issue. This worked for me:
On the schema, instead of this:
Do this:
I love u!!!!
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
Look here: https://github.com/typeorm/typeorm/issues/4190