NestJS Course Lesson 05 - Repository Pattern
NextjsVietnam
Posted on July 25, 2023
Lesson 05
- A brief introduction to the repository pattern
- Introduction to dependency injection in NestJS
- 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
The controller uses the ORM's Model directly to execute queries to the database.
This design will expose the following weaknesses:
- Reusability of queries: since it is located directly in the controller, it is almost impossible to reuse and share between different controllers.
- Increasing the complexity of the controller, the more complex queries, the bigger the controller.
- 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);
// 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();
}
}
// 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 {}
@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,
};
}
// ...
}
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();
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,
};
// 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 {}
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,
};
}
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
In the above model, the components are defined as follows:
DTO : data transfer object
- Request DTO
This part is the data received from the client
- Response DTO
This part is the data after a process of processing through different steps that will be bound to the View section
- 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.
- 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
- Entity
The main business object, including the properties that represent the object in the application
- 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.
- 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/]
Posted on July 25, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.