Clean Architecture is a software architecture pattern proposed by Robert C. Martin, aiming to separate the application into two parts: internal and external, making the system easier to understand, maintain, and extend. The architecture separates the system into four levels, from the lowest level to the highest level: entity layer, repository layer, use case layer, and presentation layer.
In this article, we will introduce how to implement Clean Architecture with Node.js, and provide some sample code to demonstrate the key concepts of the architecture.
In this architecture, the entity level contains entity objects and related business rules and logic. The repository level is responsible for persisting and retrieving data, the use case level contains the system's use cases or operations, and the presentation level is responsible for interacting with the user interface.
The Clean Architecture pattern has become increasingly popular in recent years, and for good reason. It offers a flexible and scalable architecture that can be used to build a wide range of applications. In this article, we will explore how to implement the Clean Architecture pattern in Node.js.
To demonstrate the Clean Architecture pattern in action, we will use a TypeScript project example . This project uses a Monorepo structure and is managed using Rush.js. The server folder contains three sub-projects: core, koa, and nestjs-app. The core sub-project contains the core business logic, while koa and nestjs-app use different frameworks to bridge the core logic.
Let's take a look at how the Clean Architecture pattern is implemented in this project. The basic idea is to split the application into four levels, with each level having its own responsibilities and rules to achieve better maintainability and testability.
At the core level, we have the core business logic code. This level contains the domain, repository, and use case. The domain contains the entity-related code, such as specific business models. The repository contains the related interfaces to external storage systems. The use case contains the code related to business logic, such as handling business logic, data validation, and calling the repository.
At the koa/nestjs-app level, we have the actual consumers of the core level. They implement the specific routers and repositories according to the interfaces provided by the core level.
One of the main advantages of using Clean Architecture is that it separates the business logic from the technology implementation. This means that you can easily switch between different frameworks and libraries without changing the core business logic. In our example, we can switch between koa and nestjs-app while maintaining the same core business logic.
Define the Entity Layer
// server/core/src/domain/post.ts
import { User } from "./user";
export class Post {
author: User | null = null;
content: string = "";
updateAt: Date = new Date(); // timestamp;
createdAt: Date = new Date(); // timestamp;
title: string = "";
id: number = -1;
}
// server/core/src/domain/user.ts
export class User {
name: string = ''
email: string = ''
id: number = -1
}
Define the Repository Layer
import { Post } from "../domain/post";
export interface IPostRepository {
create(post: Post): Promise<boolean>;
find(id: number): Promise<Post>;
update(post: Post): Promise<boolean>;
delete(post: Post): Promise<boolean>;
findMany(options: { authorId: number }): Promise<Post[]>;
}
...
import { User } from "../domain/user";
export interface IUserRepository {
create(user: User): Promise<boolean>;
find(id: number): Promise<User>;
}
Define the Use Case Layer
import { User } from "../domain/user";
import { IUserRepository } from "../repository/user";
export class UCUser {
constructor(public userRepo: IUserRepository) {}
find(id: number) {
return this.userRepo.find(id);
}
create(name: string, email: string) {
if (email.includes("@test.com")) {
const user = new User();
user.email = email;
user.name = name;
return this.userRepo.create(user);
}
throw Error("Please use legal email");
}
}
Implement the Repository in the koa project
// server/koa/src/user/user.repo.ts
import { PrismaClient } from "@prisma/client";
import { IUserRepository, User } from "core";
export class UserRepository implements IUserRepository {
prisma = new PrismaClient();
async create(user: User): Promise<boolean> {
const d = await this.prisma.user_orm_entity.create({
data: {
email: user.email,
name: user.name,
},
});
return !!d;
}
async find(id: number): Promise<User> {
const d = await this.prisma.user_orm_entity.findFirst({
where: {
id: id,
},
});
if (d) {
const u = new User();
u.email = d?.email;
u.id = d?.id;
u.name = d?.name;
return u;
}
throw Error("user id " + id + "not found");
}
}
Implement the controller in the koa project
// server/koa/src/user/user.controller.ts
import Router from "@koa/router";
import { UCUser } from "core";
import { UserRepository } from "./user.repo";
export const userRouter = new Router({
prefix: "/user",
});
userRouter.get("/:id", async (ctx, next) => {
try {
const service = new UCUser(new UserRepository());
if (ctx.params.id) {
const u = await service.find(+ctx.params.id);
ctx.response.body = JSON.stringify(u);
}
} catch (e) {
ctx.throw(400, "some error on get user", e.message);
}
await next();
});
An example of the nestjs project can be found in this path (https://github.com/lulusir/clean-architecture/tree/main/server/nestjs-app)
Note that in the actual project we do not put the core business logic in a separate repository, this is just for demonstration
By separating the business logic from the technology implementation, you can easily switch between different frameworks and libraries without changing the core business logic. If you are looking to build scalable and maintainable applications, then Clean Architecture is definitely worth considering.
To get started with Clean Architecture in Node.js, check out this project (https://github.com/lulusir/clean-architecture) and start exploring. Please give me a star, thank you. Happy coding!
Top comments (0)