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:
- DDD;
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
Β
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)
Β
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:
- id (optional, because at the moment of creating the record still doesn't have value)
- title
- price
Β
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, 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
and 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.Β