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

Phat Vo
8 min readAug 20, 2020

--

The previous sections:

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

In the previous sections, we have built a 3-Tier architecture and based on that we have produced an API.
In a monolithic application running on a single process, components invoke another by using language-level method and function calls. Looking back to our application the 3-Tier architecture seems still tightly coupled with methods invoke other methods between layers.

Our goal is to build a loosely coupled application and separate all the concerns.
A loosely coupled between the layers might make your application scalability and maintainable easier. Each layer takes a single responsibility and has only one reason for changes.
To communicate between the layers we will be using Dependency Injection by referencing abstraction rather than concrete object instances by methods, function calls.

So, continue to build our application by separated all the concerns and loosely coupled. Firstly, we will talk about the Repository pattern. Repository represents data tier in a 3-Tier architecture.

Let’s take a look at figure 3.1, illustrate a Repository pattern with a simple Post and User.

Figure 3.1: A typical, of the Repository pattern

Let’s going to implement the Repository Pattern in our application, look at figure 3.2, illustrate the application folder structure and coding snippets below.

Figure 3.2: Create repositories folder into the src folder

Under the repositories folder, we have a folder called the base and there are two files as abstract, interface repository situated on it.

Every repository in the application like User, Post will have to extend and implement those abstract class and interface. So, let’s have a look at the coding snippets:

base.interface.repository.ts

import { DeleteResult } from 'typeorm';

export interface BaseInterfaceRepository<T> {
create(data: T | any): Promise<T>;

findOneById(id: number): Promise<T>;

findByCondition(filterCondition: any): Promise<T>;

findAll(): Promise<T[]>;

remove(id: string): Promise<DeleteResult>;

findWithRelations(relations: any): Promise<T[]>;
}

base.abstract.repository.ts

import { BaseInterfaceRepository } from './base.interface.repository';
import { DeleteResult, Repository } from 'typeorm';

export abstract class BaseAbstractRepository<T> implements BaseInterfaceRepository<T> {

private entity: Repository<T>;

protected constructor(entity: Repository<T>) {
this.entity = entity;
}

public async create(data: T | any): Promise<T> {
return await this.entity.save(data);
}

public async findOneById(id: number): Promise<T> {
return await this.entity.findOne(id);
}

public async findByCondition(filterCondition: any): Promise<T> {
return await this.entity.findOne({where: filterCondition});
}

public async findWithRelations(relations: any): Promise<T[]> {
return await this.entity.find(relations)
}

public async findAll(): Promise<T[]> {
return await this.entity.find();
}

public async remove(id: string): Promise<DeleteResult> {
return await this.entity.delete(id);
}

}

Let’s take details in those abstract class and interface, we have declared those methods in the interface that represents for a simple CRUD (Create, Read, Update and Delete) functionalities and most application doing. The base abstract class will implement the base interface and execute each method defined.

Note*: If you are not familiar with the “T” type in both Abstract class and Interface. So then please take a look at TypeScript Generic.

Now imagine our application needs a User Repository, which is relevant in some of the user’s functionalities. Goes through the base repository, so we can be growth and expansion of User Repository rely on the base repository.

Figure 3.3: Added user.repository.interface.ts and user.repository.ts in both user component and repositories

Following the code snippets under the hood exposing a comprehensive implementation of User Repository.

user.repository.interface.ts

import { BaseInterfaceRepository } from '../../../repositories/base/base.interface.repository';
import { User } from '../entity/user.entity';

export interface UserRepositoryInterface extends BaseInterfaceRepository<User> {
}

user.repository.ts

import { BaseAbstractRepository } from './base/base.abstract.repository';
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from '../components/user/entity/user.entity';
import { UserRepositoryInterface } from '../components/user/interface/user.repository.interface';

@Injectable()
export class UserRepository extends BaseAbstractRepository<User> implements UserRepositoryInterface {

constructor(@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {
super(usersRepository);
}

}

A user repository class has to inheritance the base abstract repository and implement itself interface. The @Injectable() decorator added in user.repository shows that we can use Dependency Injection to inject to class into another layer in the application.

Back to our use case relatively in creating a new user and store in the user's collection. We now have User Repository which is situated in the data tier presentive in 3-Tier architecture.

We store our user’s data into the user’s collection that we need to invoke the user repository into the user service layer. The methods calls will be referencing to the user interface, at this point we need to apply the Dependency Injection to inject our interface into a place needed.

Fortunately, the Nest framework has been supported for Dependency Injection by declaring the class or interface needed into the provider of each module. Let’s overwhelming our functionalities by following the line of code below.

user.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entity/user.entity';
import { UserController } from './user.controller';
import { UserRepository } from '../../repositories/user.repository';
import { UserRepositoryInterface } from './interface/user.repository.interface';

@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [{
provide: 'UserRepositoryInterface',
useClass: UserRepository,
}],
controllers: [UserController],
})
export class UserModule {
}

user.service.ts

import { Inject, Injectable } from '@nestjs/common';
import { User } from './entity/user.entity';
import { UserRepositoryInterface } from './interface/user.repository.interface';

@Injectable()
export class UserService {
constructor(
@Inject('UserRepositoryInterface')
private readonly userRepository: UserRepositoryInterface,
) {
}

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

We have to declare the user repository class and interface into Provider which is located in the user module and then take a look into a user service class that we can inject user repository. The method this.userRepository.create() is referencing from the user repository interface, which means we were abstraction our functionality.

And you can see that the user service layer makes an invoke to the user repository layer by using abstraction referencing. Those two layers have mentioned above are respective to Logic tier and Data titer in 3-Tier architecture.

The cause of rising levels of abstraction is required our services as user service should growth and expansion of a user service interface. And in the user controller, we can be using method calls to user service by referencing the user service interface.

Figure 3.4: Created user.service.interface.ts under interface

Let’s take a look our codding implementation

Using class class-validator . NestJS being support validation as global by declaring global validation in main.ts

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();

components/user/dto/create-user.dto.ts

import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';

export class CreateUserDto {
@IsNotEmpty()
@IsEmail()
email: string;

@IsNotEmpty()
@MinLength(8)
password: string;
}

user.service.interface.ts

import { User } from '../entity/user.entity';
import { CreateUserDto } from '../dto/create-user.dto';

export interface UserServiceInterface {
create(userDto: CreateUserDto): Promise<User>;
}

user.service.ts

import { Inject, Injectable } from '@nestjs/common';
import { User } from './entity/user.entity';
import { UserRepositoryInterface } from './interface/user.repository.interface';
import { UserServiceInterface } from './interface/user.service.interface';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UserService implements UserServiceInterface{
constructor(
@Inject('UserRepositoryInterface')
private readonly userRepository: UserRepositoryInterface,
) {
}

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

user.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entity/user.entity';
import { UserController } from './user.controller';
import { UserRepository } from '../../repositories/user.repository';
import { UserRepositoryInterface } from './interface/user.repository.interface';
import { UserService } from './user.service';
import { UserServiceInterface } from './interface/user.service.interface';

@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [
{
provide: 'UserRepositoryInterface',
useClass: UserRepository,
},
{
provide: 'UserServiceInterface',
useClass: UserService,
},
],
controllers: [UserController],
})
export class UserModule {
}

user.controller.ts

import {
Body,
Controller, Inject,
Post,
} from '@nestjs/common';
import { User } from './entity/user.entity';
import { UserServiceInterface } from './interface/user.service.interface';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UserController {

constructor(@Inject('UserServiceInterface')
private readonly userService: UserServiceInterface) {
}

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

}

So, now you can see that we have separated all the concerns. Ther user controller take responsibility as a presentation tier, the user service handles all logic before storing data into the user’s collection. Meanwhile, the user repository is a data tier that directly interacts with our database.

Between each layer, we have using dependency injection to makes the calls to each corresponding method into the referenced interface.

Finally, let’s run up our application via the command npm run start:dev and create a new user via API exposing.

Validation error case:

Create success case:

We have created a new user successfully in another way of implementation compare to our application in section 2. Both ways actually working well but using our methodology in this section might make the application more scalable and strongly take a single responsibility of each layer.

Our application looks well, but we still need to take our application running on the development and production server as well as stable.
This is a cause why we’re remaining meet in the next section.

What Next?

We will learn how to validation our input data and use data mapper

We will learn how to build the application with PM2 in production

We will learn how to build the application in Docker

Let’s have a look at the next section.

Thanks for reading!

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

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

--

--