DEV Community

Cover image for Integrate MongoDB database with multiple collections using Mongoose in NestJS and Typescript
Elizabeth Morillo for One Beyond

Posted on

Integrate MongoDB database with multiple collections using Mongoose in NestJS and Typescript

In this article we are going to show you a way to integrate multiple MongoDB(NoSQL database) collections using Mongoose for highly scalable projects into the NestJS framework.

What are we going to need?

  • MongoDB database, you can use one locally or use the cloud MongoDB database.

  • Have a new project created with Nest CLI, you can check this article for first steps.

Installation:

Before starting with our code, it is necessary to install all the dependencies that we need, to do this in our console we run
npm install @nestjs/mongoose mongoose

Mongoose Schema

Before connecting and configuring our database we are going to create the necessary schemas. Each Schema represents a MongoDB collection that will define our models and each key will define a property in our document that will be associated with a type.
If you want to read more about it, I recommend this official guide.

In the root of our project, we are going to create the following folder structure and inside our Schema folder let’s create a file for each collection we have in our database, in my case I will have only two, it should be something like this:

src
├── api 
│   ├── database  
│       └── schemas 
│         └── user.schema.ts 
│         └── dog.schema.ts
Enter fullscreen mode Exit fullscreen mode

Now let's define our schemas:

// schemas/user.schema.ts

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';

export type UserDocument = HydratedDocument<User>;

@Schema()
export class User {
  @Prop({ required: true, unique: true })
  id: number;

  @Prop({ required: true })
  first_name: string;

  @Prop({ required: true })
  last_name: string;

  @Prop({ required: true })
  email: string;
}

export const UserSchema = SchemaFactory.createForClass(User); 
Enter fullscreen mode Exit fullscreen mode
// schemas/dog.schema.ts

import * as mongoose from 'mongoose';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
import { User } from './user.schema';

export type DogDocument = HydratedDocument<Dog>;

@Schema()
export class Dog {
  @Prop()
  name: string;

  @Prop()
  age: number;

  @Prop()
  breed: string;

  @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User' })
  owner: User;
}

export const DogSchema = SchemaFactory.createForClass(Dog);
Enter fullscreen mode Exit fullscreen mode

This looks spectacular! and we can use decorators to improve our code. The @Prop() decorator defines schema types that are automatically inferred by Typescript.
I personally prefer to use decorators since it allows us a better definition, we can send arguments and improve the typing to ensure that we do not have any errors in the future related to the data we send to our database, but if you prefer you can also create your schematics manually, Here's an example:

export const DogSchema = new mongoose.Schema({ 
  name: String, 
  breed: String, 
  age: Number, 
  owner: {type: mongoose.Types.ObjectId, ref: "User"} 
});
Enter fullscreen mode Exit fullscreen mode

HINT: Remember we don’t need to add an _id since Mongoose automatically adds an _id property to your schema.

Connection and Database Module

Once our models have been defined, we can create the connection to our database, inside the folder we created earlier called database, we are going to create a file named database.module.ts where we will make the connection to our database.

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import database from '../constants/database';

@Module({
  imports: [
    MongooseModule.forRoot('<YOUR_MONGODB_CONNECTION_STRING>', {
      dbName: database.DATABASE_NAME,
    }),
  ],
  controllers: [],
  providers: [],
})
export class DatabaseModule {}
Enter fullscreen mode Exit fullscreen mode

HINT: It is important to indicate the name of the database to which we want to connect (dbName)

Following best practices, you should create a folder named constants, where we will keep all these literal numeric or string values, known as "magic numbers" and "magic strings", In my case I have only created a database file, here is an example:

// ../api/constants/database.ts

export default {
  DATABASE_NAME: 'dev_test',
};
Enter fullscreen mode Exit fullscreen mode

There is also the possibility of creating our database connection asynchronously and for this Nest provides us with a method called forRootAsync(), this method also allows us to pass options asynchronously and inject dependencies as a ConfigModule, for example, this will be the same connection but asynchronously:

@Module({
  imports: [
    MongooseModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        uri: configService.get<string>('<YOUR_MONGODB_CONNECTION_STRING>'),
        dbName: database.DATABASE_NAME,
      }),
      inject: [ConfigService],
    }),
  ],
  controllers: [],
  providers: [],
})
export class DatabaseModule {}
Enter fullscreen mode Exit fullscreen mode

If you want to know more about how to configure and use a ConfigModule and a ConfigService using the environment variables, I recommend this article.

To be able to use the new connection we need to import the new DatabaseModule to our app.module

import { Module } from '@nestjs/common';
import { DogModule } from './api/dog/dog.module';
import { UserModule } from './api/user/user.module';
import { DatabaseModule } from './api/database/database.module';

@Module({
  imports: [DogModule, UserModule, DatabaseModule],
  controllers: [],
  providers: [],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Using our collections

Up to this point if we have our app running and listening to all the changes we have been making (npm run start:dev) we should not have any errors in the console, Excellent!

Now let's see how to consume our data stored in both collections independently. To begin we are going to create the following folders

src
├── api 
│   ├── database
│   ├── dog
│       └── dog.module.ts
│       └── dog.service.ts
│   ├── user
│       └── user.module.ts
│       └── user.service.ts
Enter fullscreen mode Exit fullscreen mode

In this example I only need two modules, but the idea is that you create how many modules you need according to your collections.

It’s important in this step that if we have multiple collections, we should always indicate the name of the schema to which we want to refer, the method forFeature() provided by the MongooseModule allow us to configure the module, the models by including them should be registered in the current scope.

// dog.module.ts

import { Module } from '@nestjs/common';
import { DogService } from './dog.service';
import { DogController } from './dog.controller';
import { MongooseModule } from '@nestjs/mongoose';
import { Dog, DogSchema } from 'src/api/database/schemas/dog.schema';

@Module({
  imports: [MongooseModule.forFeature([{ name: Dog.name, schema: DogSchema }])],
  controllers: [DogController],
  providers: [DogService],
})
export class DogModule {}
Enter fullscreen mode Exit fullscreen mode
// user.module.ts

import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from 'src/api/database/schemas/user.schema';

@Module({
  imports: [
    MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
  ],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}
Enter fullscreen mode Exit fullscreen mode

NOTE: Remember to Add your modules to app.module.ts in the imports

Now we are ready for the queries!

As you can see, both models are practically the same, the only difference is that they use different collections, having registered our schema allows us to inject using the @InjectModel() decorator in the service that we want to use

// dog.service.ts

import { Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Dog } from 'src/api/database/schemas/dog.schema';

@Injectable()
export class DogService {
  constructor(@InjectModel(Dog.name) private dogModel: Model<Dog>) {}

  async findAll(): Promise<Dog[]> {
    Logger.log('This action returns all DOGS');
    return this.dogModel.find();
  }
}
Enter fullscreen mode Exit fullscreen mode

Amazing, isn't it? This will not only allow us to have a more organized project but thinking about the future when refactoring/deleting/adding collections will be much easier.

Now wait a moment, what would happen if, for example, our dog service also needed to use data from the User’s collection? Would it be possible to add a second collection to the dog module? Yes, it is possible!

Keep in mind that for these cases it is always better to create fields that make references between the collections, this is an example of how we would inject the user collection into our Dog module:

// dog.module.ts

import { Module } from '@nestjs/common';
import { DogService } from './dog.service';
import { DogController } from './dog.controller';
import { MongooseModule } from '@nestjs/mongoose';
import { Dog, DogSchema } from 'src/api/database/schemas/dog.schema';
import { User, UserSchema } from '../database/schemas/user.schema';

@Module({
  imports: [
    MongooseModule.forFeature([
      { name: Dog.name, schema: DogSchema },
      { name: User.name, schema: UserSchema },
    ]),
  ],
  controllers: [DogController],
  providers: [DogService],
})
export class DogModule {}
Enter fullscreen mode Exit fullscreen mode
// dog.service.ts

import { Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Dog } from 'src/api/database/schemas/dog.schema';
import { User } from '../database/schemas/user.schema';

@Injectable()
export class DogService {
  constructor(
    @InjectModel(Dog.name) private dogModel: Model<Dog>,
    @InjectModel(User.name) private readonly userModel: Model<User>,
  ) {}

  async findAll(): Promise<Dog[]> {
    Logger.log('This action returns all DOGS');
    return this.dogModel.find();
  }

  async findAllUsers(): Promise<User[]> {
    Logger.log('This action returns all USERS in dog service');
    return this.userModel.find();
  }
}
Enter fullscreen mode Exit fullscreen mode

Thank you all very much for reading this article and I hope it helps you create amazing applications. Please let me know in the comments what you think about this post or if you have any questions. Thank you so much for reading!

Thank you all and happy coding! 🖖

Top comments (0)