Hello, dev! Let’s start one more sequence of exciting technical posts related to the NestJS ecosystem, and in this sequence, I am going to review and exercise with you concepts like:
The idea is to only go through some parts of the application, ok? I recommend this post for those who have some knowledge about NestJS already and want to see & apply a different architectural approach to have applications with as little coupling as possible.
There’s another post that I wrote in the past, that also applied this approach of less coupling. If you want to take a look the link is
Creating Smart Questions with NestJS and OpenAI
What is going to be the project?
This is a simple project that represents part of an e-commerce ecosystem. We are going to design a solution with four basic entities, they are
Plus, we will integrate this solution with the Stripe payment platform and a Cache layer for the GET endpoints to respond faster!
Start
For this project, I am going to use a few different technologies for two reasons:
-
I want to share how Clean Architecture can help a project not rely on the technology; For example, if you want to change the database, or payment provider… it is going to be very simple.
-
To have a clear project pattern applied;
The technologies are going to be:
-
NestJS
-
MongoDB
-
Postgres
-
SWC
I am starting this project, following the same steps that I have shown in this post (I just skipped the deploy step)
Implementing auth flow as fast as possible using NestJS
With the project created and the setup done, we can organize the folders and the main files. The final version is gonna be like this:
assets
├── prisma
└── src
├── application
│ └── ecommerce
│ ├── ports
│ └── use-case
├── core
│ └── entities
├── domain
│ └── ecommerce
└── infra
├── env
├── http
│ └── dto
├── payment
│ └── stripe
└── persistence
├── cache
│ └── interceptor
├── mongoose
│ ├── entities
│ ├── mapper
│ └── repositories
└── prisma
├── mapper
└── repositories
To start with something visual, let’s start with the infra/http This HTTP, represents one of the external layers that compound the architecture, so, everything related to external technologies, for example, databases, presenters, and libraries, will be inside this folder.
To make things clear, we are following this diagram. (don’t worry about everything else yet)
As HTTP is a module, let’s create a module and its related files
http
├── app.controller.ts
├── checkout.controller.ts
├── dto
│ ├── create-order-product.dto.ts
│ ├── create-order.dto.ts
│ ├── create-product.dto.ts
│ └── create-user.dto.ts
├── http.module.ts
├── order.controller.ts
├── product.controller.ts
├── user.controller-e2e-spec.ts
└── user.controller.ts
This is the final version with everything inside, but you can start off with the http.module.ts and product.controller.ts.
The module is pretty simple, he initially is going to have the product.controller declared in it and speaking about the product.controller, you can create a simple GET method just to have something in it as well. (you can find the final version here).
With the HTTP module created, you can add it to the app.module and make things work.
Domain entities
As you might have seen, we have a folder called src/domain/ecoomerce, and in there we are declaring the classes that will represent the three entities that I mentioned in the beginning.
ecommerce
├── order-product.ts
├── order.ts
├── product.ts
└── user.ts
Each one is a Class and has its properties, for example, products
In this case, I am declaring a Product class and this domain contains fields, they are:
Other entities
The other entities are kind of similar to the Product, however, if we take a look at Order, we will see that this one has a relation to others like orderProduct.
import { Entity } from "@app/core/entities/entity";
import { OrderProduct } from "./order-product";
export interface OrderProps {
id?: string;
user: string
total?: number
status?: "paid" | "open" | "canceled"
paymentId?: string,
paymentMethod?: "stripe" | "paddle" | "paypal" | "other", // It is only working with stripe for now
orderProduct?: OrderProduct[]
}
export class Order extends Entity<OrderProps> {
constructor(props: OrderProps) {
props.total = props.total ?? 0;
props.status = props.status ?? "open";
super(props);
}
...
(The final folder with all the entities → here)
The reason for those relations is simple, an order in this context has one or more products. One more detail that this entity knows can be seen in the construction method. If this Domain is declared without total and status defined, it will assume 0 as price and “open” as status. We could have more methods or conditions that we understand are correct for the domain if that is the case, okay?
Just to reinforce, when we talk about Domain, we are referring to the main layer of the diagram.
Connecting the layers
Following the diagram, we have to follow the sequence:
Controller → Use case → Entities
During usage, we can use anything from the external layer in order to execute the logic, and all of this is held in the use case, so we are going to connect the layers inside the HTTP + Use cases, especially because of the dependency injection that NestJS has.
Check out this final version of the http.module
As I said, we have the Controllers that are going to receive the use cases, and the use cases rely on the Payment module and Database module. One detail, The Database module is declared as global , and for that reason, it is not included here. The database module is declared in the app.module like this final version here.
It is important to understand the dependency injection and the module strategy that NestJS applied to understand the most from this example.
To have a better understanding of all of these, let’s see the following files
This is the flow that the request follows when it is requested.
Conclusion
We have covered a bunch of parts of this application and concepts so far, and as I said, it is a sequence of posts. For the next one, we are going to see and understand more about abstractions how to create a module, service, and functionality with as little coupling as possible. The idea is to apply this technique for the use cases and the repositories that later are going to persist the information in the database.