Hexagonal Architecture
The Hexagonal Architecture proposes that our domain is the core of the layers and that it is not coupled to anything external. Instead of making explicit use of the principle of dependency inversion, we couple to contracts (interfaces or ports) and not to concrete implementations.
Clean Architecture
Clean Architecture is a term introduced by Robert C. Martin, better known as Uncle Bob. He compiled the most used layered models in an improved version that he called Clean Architecture, basically it offers a series of best practices when designing our application, where the domain should be at the center and should not depend on anything external (Infrastructure).
DDD
They are a set of patterns, principles and practices that help us to understand, solve and design the best solutions to business problems (Domain), applying object-oriented design. Focusing first on discovering the objects of our domain in order to continue with the design of the other layers of our application.
It can be concluded that the concepts are strongly related, so whichever one is implemented is a good practice. In these architectures the following layers stand out:
Domain
It can contain any Interface, Type, Enum or Constant that is important to our business, and can define the operation of the outermost layers.
Entities: These are interfaces that are the representation of our business objects, for example: Users, Products and Orders.
Repositories: They are interfaces that define the methods with our entities, for example: findAll, findByid, create, update, delete.
Application
This layer must contain all the business logic. They are Classes or functions that interact with our domain entities, according to your preference some call them use cases or other services. These classes must implement repositories or any class injected as a dependency.
Infrastructure
It is the last layer of our architecture in it must be: Frameworks, Libraries. These implement the input and output connections of our application such as: Http, Graphql, Databases.
Repositories: They usually implement the connections to data sources, such as databases or APIs.
Input and output: receive and send the data of our application generally called Controllers.
The following is an example of an API code with this architecture.
Structure Project
─ src
├─ application
│ └─ services
│ └─ users.service.ts
├─ domain
│ ├─ entities
│ │ └─ users.entity.ts
│ └─ respositories
│ └─ users.repository.ts
└─ infrastructure
├─ express
│ ├─ controllers
│ │ └─ users.controller.ts
│ └─ server
│ └─ server.express.ts
└─ sequelize
├─ config.sequelize.ts
├─ models
│ └─ users.model.ts
└─ respositories
└─ users.repository.ts
Dependencies required for this project
npm i -D @types/express @types/node @types/validator @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint nodemon prettier typescript
npm i express reflect-metadata sequelize sequelize-typescript sqlite3
Domain -> Entities -> user.entity.ts
export interface IUser {
id: number
username: string
password: string
firstName: string
lastName: string
}
Domain -> Repositories-> users.repository.ts
import { IUser } from "../entities/users.entity"
// get one user
type result = [IUser | null, Error | null]
// get many users
type results = [IUser[] | null, Error | null]
export interface IUsersRepository {
findAll: () => Promise
findById: (id: number) => Promise
create: (user: any) => Promise
}
Application -> Services -> users.service.ts
import { IUser } from "../../domain/entities/users.entity"
import { IUsersRepository } from "../../domain/respositories/users.repository"
import { UsersRepositoryIns } from "../../infrastructure/sequelize/respositories/users.repository"
class UsersService {
_repository: IUsersRepository
constructor(repository: IUsersRepository) {
this._repository = repository
}
async findAll() {
const [users, err] = await this._repository.findAll()
if (err) {
return { code: 500, data: "error getting users " }
}
return { code: 201, data: users }
}
async findById(id: number) {
const [user, err] = await this._repository.findById(id)
if (err) {
return { code: 500, data: "error getting the user" }
}
if (!user) {
return { code: 404, data: "user not found" }
}
return { code: 201, data: user }
}
async create(userData: IUser) {
const [user, err] = await this._repository.create(userData)
if (err) {
return { code: 500, data: "error creating the user" }
}
return { code: 201, data: user }
}
}
// Inject Repository
export const UsersServiceIns = new UsersService(UsersRepositoryIns)
Infrastructure -> Sequelize -> Repositories -> users.repository.ts
import { IUser } from "../../../domain/entities/users.entity"
import { IUsersRepository } from "../../../domain/respositories/users.repository"
import Users from "../models/users.model"
class UsersRepository implements IUsersRepository {
async findAll() {
try {
const user = await Users.findAll()
return [user, null] as [IUser[], null]
} catch (e) {
console.error(e)
return [null, e] as [null, Error]
}
}
async findById(id: number) {
try {
const user = await Users.findByPk(id)
return [user, null] as [IUser, null]
} catch (e) {
console.error(e)
return [null, e] as [null, Error]
}
}
async create(userData: IUser) {
try {
const user = await Users.create(userData)
return [user, null] as [IUser, null]
} catch (e) {
console.error(e)
return [null, e] as [null, Error]
}
}
}
export const UsersRepositoryIns = new UsersRepository()
Infrastructure -> Sequelize -> Models -> users.model.ts
import { Table, Column, Model } from "sequelize-typescript"
import { IUser } from "../../../domain/entities/users.entity"
type IUserModel = Omit
@Table
export default class Users extends Model implements IUserModel {
@Column
username: string
@Column
password: string
@Column
firstName: string
@Column
lastName: string
}
Infrastructure -> Sequelize -> config.sequelize.ts
import { Sequelize } from "sequelize-typescript"
export const sequelizeIns = new Sequelize({
dialect: "sqlite",
models: [__dirname + "/models"],
storage: __dirname + "/../../../db/app.db.sqlite",
logging: false,
})
;(async () => {
await sequelizeIns.sync({ alter: true })
console.log("Model Sync......")
})()
Infrastructure -> Express -> Controllers -> users.controller.ts
import express from "express"
import { UsersServiceIns } from "../../../application/services/users.service"
export const UsersController = express.Router()
UsersController.get("/", async (req, res) => {
const { code, data } = await UsersServiceIns.findAll()
return res.status(code).json(data)
})
UsersController.get("/:id", async (req, res) => {
const { code, data } = await UsersServiceIns.findById(+req.params.id)
return res.status(code).json(data)
})
UsersController.post("/", async (req, res) => {
const { code, data } = await UsersServiceIns.create(req.body)
return res.status(code).json(data)
})
Infrastructure -> Express -> server.express.ts
import express from "express"
import "../../sequelize/config.sequelize"
import { UsersController } from "../controllers/users.controller"
const app = express()
app.use(express.json())
app.use(express.urlencoded())
const port = 3000
app.get("/", (_, res) => {
res.send("API Clean Hexagonal DDD Architecture")
})
app.use("/users", UsersController)
app.listen(port, () => {
console.log("Run server....", port)
})
this publication is an update to the previous publication on hexagonal architecture. https://fullkernel.blogspot.com/2021/09/hexagonal-architecture-api-with-node.html
Repository: https://github.com/sistemasnegros/api-clean-hexagonal-ddd-architecture
Referencias:
Alistair Cockburn - Articule https://alistair.cockburn.us
Robert C. Martin - Clean Arquitecture( Book).
Eric Evans - Domain-Driven Design (Book).
0 Comments