DEV Community

NextjsVietnam
NextjsVietnam

Posted on

NestJS Course Lesson 05 - Repository Pattern

Lesson 05

  1. A brief introduction to the repository pattern
  2. Introduction to dependency injection in NestJS
  3. Refactor the PetCategory code applying the repository pattern

Brief introduction to repository pattern

In the previous post, you can see the following diagram in the MVC pattern

image

The controller uses the ORM's Model directly to execute queries to the database.
This design will expose the following weaknesses:

  1. Reusability of queries: since it is located directly in the controller, it is almost impossible to reuse and share between different controllers.
  2. Increasing the complexity of the controller, the more complex queries, the bigger the controller.
  3. Unit Test implementation becomes difficult, because the database query part is mixed into the controller's code

To solve this problem, you should divide the code into different layers. The aim is to help increase the scalability and maintainability of the project.

That's why the Repository Pattern is applied. To the definition of

Repository is a class born to do the task of performing queries to the database.

Because of the above characteristics, it only performs a single task, which is querying the database. Complex queries are encapsulated in this class. As a result, the 3 problems mentioned above will be thoroughly solved.

You can read more about Repository in this article about Domain Driven Design.

Here, let's apply the above definition to practice with NestJS.
In the previous post, the Model got them directly in the Controller as follows

const petCategories = await PetCategory.findAll();
await PetCategory.destroy({ where: { id } });
const newPetCategory = await PetCategory.create({ ...object });
const petCategory = await PetCategory.findByPk(id);
await petCategory.update(object);
Enter fullscreen mode Exit fullscreen mode
// src\pet\repositories\pet-category.repository.ts
import { Injectable } from "@nestjs/common";
import { PetCategory } from "../models/pet-category.model";

@Injectable()
export class PetCategoryRepository {
   findAll() {
     return PetCategory.findAll();
   }
}
Enter fullscreen mode Exit fullscreen mode
// src/pet/pet.module.ts

import { Module } from "@nestjs/common";
import { PetController } from "./controllers/pet.controller";
import { ManagePetController } from "./controllers/admin/manage-pet.controller";
import { ManagePetCategoryController } from "./controllers/admin/manage-pet-category.controller";
import { ManagePetAttributeController } from "./controllers/admin/manage-pet-attribute.controller";
import { nestjsFormDataModule } from "nestjs-form-data";
import { PetCategoryRepository } from "./repositories/pet-category.repository";
@Module({
   imports: [NestjsFormDataModule],
   controllers: [
     PetController,
     ManagePetController,
     ManagePetCategoryController,
     ManagePetAttributeController,
   ],
   // registered providers
   providers: [PetCategoryRepository],
})
export class PetModule {}
Enter fullscreen mode Exit fullscreen mode
@Controller("admin/pet-categories")
export class ManagePetCategoryController {
   constructor(private petCategoryRepository: PetCategoryRepository) {}
   @Get("")
   @Render("pet/admin/manage-pet-category/list")
   async getList() {
     const petCategories = await this.petCategoryRepository.findAll();
     return {
       petCategories,
     };
   }
   // ...
}
Enter fullscreen mode Exit fullscreen mode

Brief introduction to dependency injection

Note in the above 3 code snippets, to use the repository, as you can see, you need to do 3 things:

  • Declare the repository as an Injectable type
  • Declare the repository for the Providers list of the module
  • Inject the repository into the constructor of the controller you want to use

If you are unfamiliar with the above concepts, please learn more details in the article about NestJS Provider.

To put it simply, when you want to use a class in a module, instead of having to automatically initialize each time you use it, this will inadvertently cause this class to be initialized too many times unnecessarily. As well as writing code will become quite confusing.

Instead, if you apply the Dependency Injection pattern, you can easily solve this problem. Example of how dependency injection solves the above problem.

import "reflect-metadata";
import { injectable, inject, container } from "tsyringe";

type ID = string | number;
interface Repository<T> {
   findOne(id: ID): T;
}
interface CrudService<Model> {
   findOne(id: ID): Model;
}

class User {
   id!: ID;
   firstName!: string;
   lastName!: string;
   constructor(payload: Partial<User>) {
     Object.assign(this, payload);
   }
}

class Role {
   id!: ID;
   name!: string;
   permissions: string[] = [];
   constructor(payload: Partial<Role>) {
     Object.assign(this, payload);
   }
}

class UserRepository implements Repository<User> {
   findOne(id: ID): User {
     const user = new User({
       id,
       firstName: "Typescript",
       lastName: "Master Class",
     });
     return user;
   }
}

class RoleRepository implements Repository<Role> {
   findOne(id: ID): Role {
     const role = new Role({
       id,
       name: "Admin",
       permissions: ["CreateUser", "EditUser", "RetrieveUser", "DeleteUser"],
     });
     return role;
   }
}

abstract class BaseService<M, R extends Repository<M>>
   implements CrudService<M>
{
   constructor(private repository: R) {}
   findOne(id: ID): M {
     return this.repository.findOne(id);
   }
}

@injectable()
class UserService extends BaseService<User, UserRepository> {
   constructor(
     @inject(UserRepository.name) userRepository: UserRepository,
     @inject(RoleRepository.name) private roleRepository: RoleRepository
   ) {
     super(userRepository);
   }

   retrievePermission(user: User) {
     return this.roleRepository.findOne(user.id);
   }
}

const main = () => {
   container.register("UserRepository", {
     useClass: UserRepository,
   });
   container.register("RoleRepository", {
     useClass: RoleRepository,
   });
   const userService = container.resolve(UserService);
   const user = userService.findOne(1);
   const permissions = userService.retrievePermission(user);
   console.log(user, permissions);
};

main();
Enter fullscreen mode Exit fullscreen mode

In the code illustrated above, you can clearly see, the service will depend on the repository. However, the initialization of the repository will be delegated. To better understand this pattern, you can learn in detail through the article Learn Enough Oop to Be Dangerous

Thus, it can be seen that when working with NestJS, creating a class and embedding another class to use is quite simple, isn't it.

In addition to creating a repository actively as above, in the nestjs ecosystem, you can use the following way. Use the model itself as a repository

Create provider using custom token and useValue

The reason is that this repository will use the static methods of the model. So initializing this dependency will use the Model itself as the value.

import { PetCategory } from "../models/pet-category.model";

export const PetCategoryInjectionKey = "Pet_Category";
export const PetCategoryProvider = {
   provide: PetCategoryInjectionKey,
   useValue: PetCategory,
};
Enter fullscreen mode Exit fullscreen mode
// src/pet/pet.module.ts

import { Module } from "@nestjs/common";
import { PetController } from "./controllers/pet.controller";
import { ManagePetController } from "./controllers/admin/manage-pet.controller";
import { ManagePetCategoryController } from "./controllers/admin/manage-pet-category.controller";
import { ManagePetAttributeController } from "./controllers/admin/manage-pet-attribute.controller";
import { nestjsFormDataModule } from "nestjs-form-data";
import { PetCategoryRepository } from "./repositories/pet-category.repository";
import { PetCategoryProvider } from "./providers/pet-category.provider";
@Module({
   imports: [NestjsFormDataModule],
   controllers: [
     PetController,
     ManagePetController,
     ManagePetCategoryController,
     ManagePetAttributeController,
   ],
   providers: [PetCategoryRepository, PetCategoryProvider],
})
export class PetModule {}
Enter fullscreen mode Exit fullscreen mode
import { PetCategoryInjectionKey } from './../../providers/pet-category.provider';
import {
   Body,
   controller,
   Get,
   render,
   Inject,
} from '@nestjs/common';
@Controller('admin/pet-categories')
export class ManagePetCategoryController {
   constructor(
     @Inject(PetCategoryInjectionKey)
     private defaultPetCategoryRepository: typeof PetCategory,
   ) {}
   @Get('')
   @Render('pet/admin/manage-pet-category/list')
   async getList() {
     const petCategories = await this.defaultPetCategoryRepository.findAll();
     return {
       petCategories,
     };
   }
Enter fullscreen mode Exit fullscreen mode

However, method #2 does not completely solve the problem like method #1, although it seems that an additional class repository appears when used in a controller. But writing the query will still be entirely in the controller.
Although it looks like the controller and model will be more independent, the essence of the problem is still not solved.
Therefore, when applied in real projects, you should choose method 1, because there will be many cases where complex queries are needed, instead of just CRUD.

Refactor code for Pet Website application

In this section, you will practice separating code into separate classes, to ensure that the application's code base has a consistent, coherent structure, and has a logical separation that helps the code base become:

  • Easy to maintain
  • Easy to test
  • Easy to add new/change

image

In the above model, the components are defined as follows:

DTO : data transfer object

  1. Request DTO

This part is the data received from the client

  1. Response DTO

This part is the data after a process of processing through different steps that will be bound to the View section

  1. Controller

This part is the part that receives data from the client side and performs the next steps in the process. Responsible for connecting to the service to get the ResponseDTO and read the template from the View and render the final result to the user.

  1. Service

This part is responsible for receiving data from the Controller and executing the business logic. Including: checking input data, transforming data if necessary, connecting with other services to perform processing operations, connecting to corresponding repositories to interact with the stored data layer. The goal is to provide aggregated data after business execution for the Controller

  1. Entity

The main business object, including the properties that represent the object in the application

  1. Model

Object mapping of data stored in the database.

Usually, it is possible to use the same class for both Entity and Model because these two objects are quite similar.

  1. Repository

This middle class is responsible for performing queries to the database

Following the above structure, please refactor the code of Pet Website.

Feel free to read full article about NestJS Course Lesson 05 - Repository Pattern

khóa học nestjs, khoa hoc nestjs, nextjs vietnam, [nestjs tips and tricks)[https://nextjsvietnam.com/]

Top comments (0)