Integrate MongoDB database with multiple collections using Mongoose in NestJS and Typescript
Elizabeth Morillo
Posted on October 25, 2023
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
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);
// 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);
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"}
});
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 {}
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',
};
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 {}
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 {}
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
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 {}
// 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 {}
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();
}
}
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 {}
// 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();
}
}
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! 🖖
Posted on October 25, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 25, 2023