How to build a scalable, maintainable application with NestJs + MongoDB apply the design patterns and run in Docker(Part 2)

Phat Vo
8 min readAug 18, 2020

--

The previous section (Part 1): https://medium.com/@phatdev/how-to-build-a-scalable-maintainable-application-with-nestjs-mongodb-apply-the-design-patterns-2f71c060652

In the last section, we have flicked through 3-tier architecture and also has dug in Nest architecture, a typical of MVC architecture by running up the Nest application.

So now we can conceivable what’s a 3-tier adequate architecture. And, this section we will hit in intensive technicality. How to custom the Nest MVC to a 3-tier architecture.

There is an abundance of database support in the Nest framework. But in this tutorial, we will go with MongoDB is a NoSQL database that stores data as JSON-like documents.

Henceforward, let’s going to config MongoDB into our application.

Cause there are ranges of benefits that Dotenv carrying. So, let’s install the Dotenv package in our application.
Dotenv is a zero-dependency module that loads environment variables from a .env file.

  • Install Dotenv: npm i --save dotenv and npm i @types/dotenv
  • There is a @nestjs/config dependency need to install along with dotenv:npm i --save @nestjs/config

After installing these dependencies successfully. Let’s going to init the config module as a global in our application. Following the code snippet below to import the config module in your application in the correct way.

  • Import config module into app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
],
controllers: [],
providers: [],
})
export class AppModule {}

We have spoken a lot about MongoDB, so time to configuration MongoDB in our application.
There is a tremendous of ORM (Object-relational Mapping), which is support in MongoDB. But in this tutorial, I tend to lean towards TypeORM. This can be attributed to a host of reasons to choose TypeORM, but probably a reasonably reasoned that TypeORM is familiar to Typescript rather than the other ones.

Let’s install MongoDB and TypeORM dependencies via the following the commands below:

  • Install MongoDB: npm i mongodb --save
  • Install TypeORM: npm i typeorm --save
  • Install @nestjs/typeorm: npm i -save @nestjs/typeorm

After installing those things successfully. Now let’s go to connect MongoDB in our application.

Makes sure that you have MongoDB server ready on your local machine and normally mongo is run on port 27017 .

Let’s have a configuration in our code via the following steps below:

  • Create a file called.envfile in the root folder, this file should be ignored by git. Cause, which contains our secret keys and data. Look at figure 2.1 shows as below.

Figure 2.1: .env created into the root folder

All the value store into this file will automatically load once we start our application.

  • Let’s create the folder database/config into src folder, where contains our DB configuration info. Take a look at figure 2.2

Figure 2.2: created database/config folders and ormconfig.ts file in src

The ormconfig.ts will have the snippet code as show as below

export function ormConfig(): any {
return {
type: process.env.DATABASE_TYPE,
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT),
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
synchronize: true,
logging: false,
autoLoadEntities: true,
useUnifiedTopology: true,
useNewUrlParser: true,
connectTimeout: parseInt(process.env.DATABASE_CONNECTION_TIME_OUT),
acquireTimeout: parseInt(process.env.DATABASE_ACQUIRE_TIME_OUT),
extra: {
connectionLimit: parseInt(process.env.DATABASE_CONNECTION_LIMIT),
},
entities: [
'dist/**/entity/*.entity.js',
],
migrations: [
'dist/database/migrations/*.js',
],
subscribers: [
'dist/observers/subscribers/*.subscriber.js',
],
cli: {
entitiesDir: 'src/components/**/entity',
migrationsDir: 'src/database/migrations',
subscribersDir: 'src/observers/subscribers',
},
};
}

The process.env.DATABASE_TYPE ..etc will be getting the config values from .env file from the root folder. Let’s take a look at figure 2.2.

Figure 2.3: The parameter and value define in .env file

  • And next step we need to import the database configuration info into app.module.ts following to the snippet below
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ormConfig } from './database/config/ormconfig';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRoot(ormConfig()),
],
controllers: [],
providers: [],
})
export class AppModule {}

Finally, we can run the command npm run start:dev to check our application running well.

So now, we have the application connected to MongoDB and running in PORT 3003. You can config server PORT loading from .env into main.ts file via the snippet below:

const port = parseInt(process.env.SERVER_PORT);
await app.listen(port);

Probably I lose track of the time and talked a bit longer about the environmentand connection. Now the timing is right to ubiquitously 3-Tier architecture in our application, but we don’t need to hectic.

Let’s refactor all files inside thesrc folder, except database folder. Let’s assume that your application needs the login, register functionalities. So, we have to define an entity (model) called users, this model represents a users collection in MongoDB.
On the other hand, we also need to implement authentication and authorization for our application.

Look at figure 2.4, illustrate the refactored folder structure to disposition the 3-Tier in our application.

Figure 2.4: Folder structure performs each module and approach to a 3-Tier architecture.

Back to the requirements, we have assumed above that the folder structure has been created and as you can see the illustration we have two modules called user and auth . The user module performs all the logic relevant to user collection. Meanwhile, the auth module represents for authentication and authorization functionalities. Both modules need to be imported into the app module to expose the intention.

Meanwhile, the components folder contains all features and functionalities needed. we also have some other folder mentioned outside of components like constant the folder is shown in figure 2.4 above. Where the constant.ts file created to allow defined something unchangeable inside it. In this case, I have defined the API Prefix into it. So, take a look at snippets code in main.ts file where we can apply API Prefix as global.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { APIPrefix } from './constant/common';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
app.setGlobalPrefix(APIPrefix.Version);
const port = parseInt(process.env.SERVER_PORT);
await app.listen(port);
}

bootstrap();

We have made the structure and somethings are indispensable in the application. So, now we can create a user entity user.entity.ts , do a migration, and an API to allow create new users stored in the database.

Let’s install bcrypt npm i bcrypt --save package for hash password and following the snippet code below to define our users' entity.

import {
BeforeInsert,
Column,
Entity, ObjectIdColumn,
} from 'typeorm';
import { hashSync, genSaltSync } from 'bcrypt';

@Entity({name: 'users'})
export class User {
@ObjectIdColumn()
id: number;

@Column({
type: 'string',
unique: true
})
email: string;

@Column({
type: 'string'
})
password: string;

@Column({
type: 'date'
})
createdAt: any;

@Column({
type: 'date'
})
updatedAt: any;

@BeforeInsert()
async hashPassword() {
this.password = await hashSync(this.password, genSaltSync(10));
}
}

If you are not familiar with TypeORM then you can have a look into this doc (https://typeorm.io/#/mongodb).

In the snippet above we have declared some straightforward fields as email , password ..etc where theBeforeInsert decorator is an event listener, which is listening to an inserting event and do hash password before import data into the user's collection. After initializing the user's entity, we need to run and declare the TypeORM feature and import user module into app.module as those lines of code below.

user.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entity/user.entity';
import { UserService } from './user.service';
import { UserController } from './user.controller';

@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UserService],
controllers: [UserController]
})
export class UserModule {}

app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ormConfig } from './database/config/ormconfig';
import { AuthModule } from './components/auth/auth.module';
import { UserModule } from './components/user/user.module';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRoot(ormConfig()),
AuthModule,
UserModule
],
controllers: [],
providers: [],
})
export class AppModule {}

So then we need to run the command npm run start:dev for database migration, this means that the user's collection has been created into a database.

Now, let’s heading to create a create user API, which is implemented in user controller and service . Following the code snippet below.

user.controller.ts

import {
Body,
Controller,
Post,
} from '@nestjs/common';
import { User } from './entity/user.entity';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {

constructor(private readonly userService: UsersService) {}

@Post()
public async create(@Body() userDto: any): Promise<User> {
return await this.userService.create(userDto);
}

}

user.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entity/user.entity';

@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}

public async create(userDto: any): Promise<User> {
const user = new User();
user.email = userDto.email;
user.password = userDto.password;
return this.userRepository.save(user);
}
}

We can foreseeable the User Controller represents for the presentation layer, where User Service respectively logic layer and User Entity could be a data layer. We can conceivably a 3-Tier operate something like that.

Let’s run the application npm run start:dev and create a user via API Create User. Our application running on http://localhost:3003 with API prefix equal api/v1 . Let’s see how it works in figure 2.5

Figure 2.5: Invoke API Create User in the Postman.

Finally, our first API has been working like a charm but take a look back to the 3-Tier architecture that we have been implemented, is that a correct implementation?. All of us have doubts and wonder whether the architecture we built working well in case we need to scale our application?.

I can say that our implementations are not wrong, but the current design will have an adverse effect on the application in case needed a scale.

So, to perform the most quintessential of 3-Tier architecture we will need to apply some patterns. Which is proportionately and plays a pivotal role in separate the concerns of the application.

We will be moving to the next section to see what’s this happens and how to apply the design patterns in our application.

What Next?

We will learn how about dependency injection

We will learn how to apply Repository pattern in the application

We will learn how to use Abstraction to abstract the application is scalable.

We will custom our 3-Tier by applying the patterns.

Let’s have a look at the next section.
Thanks for reading!

The next section (part3): https://medium.com/@phatdev/how-to-build-a-scalable-maintainable-application-with-nestjs-mongodb-apply-the-design-patterns-7b287af61354

Github source: https://github.com/phatvo21/nest-demo

List of content:

Part 1: https://medium.com/@phatdev/how-to-build-a-scalable-maintainable-application-with-nestjs-mongodb-apply-the-design-patterns-2f71c060652

Part 2: https://medium.com/@phatdev/how-to-build-a-scalable-maintainable-application-with-nestjs-mongodb-apply-the-design-patterns-50112e9c99b4

Part 3: https://medium.com/@phatdev/how-to-build-a-scalable-maintainable-application-with-nestjs-mongodb-apply-the-design-patterns-7b287af61354

Final Part: https://medium.com/@phatdev/how-to-build-a-scalable-maintainable-application-with-nestjs-mongodb-apply-the-design-patterns-789df9782959

--

--