Arquitetura Hexagonal em TypeScript: arquitetura limpa para aplicações flexíveis

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

Por que a Arquitetura Hexagonal não é apenas um termo da moda, mas uma salvação para o seu código?

Imagine que você está construindo uma casa. Você não embute as tomadas nas paredes de concreto, certo? Primeiro você passa a fiação, faz a marcação e depois conecta cuidadosamente as tomadas. No desenvolvimento de software é exatamente a mesma coisa — sua lógica de negócio (núcleo da aplicação) não deve depender de como ela é acessada: REST API, banco de dados ou filas de mensagens.

A Arquitetura Hexagonal (também conhecida como "Portas e Adaptadores", ou "Arquitetura Limpa" na interpretação de Alistair Cockburn) resolve exatamente esse problema. Neste artigo, vamos analisar como implementar essa arquitetura em TypeScript com exemplos reais de código. Você aprenderá como separar a lógica de negócio da infraestrutura, tornando o código testável e flexível.



O que é Arquitetura Hexagonal em termos simples?

Imagine um hexágono. No centro está sua aplicação com a lógica de negócio. Em cada face há uma "porta" (interface) através da qual o mundo externo pode se comunicar com o núcleo. Os adaptadores são implementações concretas: controladores, repositórios, clientes de serviços externos. Eles se conectam às portas como um plugue em uma tomada.

O princípio principal: as dependências apontam para dentro. O código externo (bancos de dados, frameworks) depende do núcleo, e não o contrário. Isso permite trocar o banco de dados de PostgreSQL para MongoDB sem tocar em uma única linha da lógica de negócio.



Conceitos principais

  • Núcleo (Domain) — lógica de negócio, entidades, value objects, casos de uso. Não possui dependências externas.
  • Porta (Port) — interface (em TypeScript é uma classe abstrata ou interface) que define o contrato para o mundo externo.
  • Adaptador (Adapter) — implementação concreta da porta. Por exemplo, um repositório que trabalha com PostgreSQL via TypeORM.
  • Application — camada que coordena a execução dos casos de uso.


Exemplo prático: sistema de gerenciamento de pedidos

Vamos projetar um sistema simples de pedidos. Teremos as entidades Order e Product, o caso de uso CreateOrder, uma porta para o repositório e um adaptador para PostgreSQL.



Passo 1. Definindo o núcleo (Domain layer)

Criaremos a pasta domain/. Aqui vivem as entidades e interfaces para as portas. Sem dependências de frameworks!



// domain/order.entity.tsexport class Order {    constructor(        public readonly id: string,        public items: OrderItem[],        public status: 'pending' | 'confirmed' | 'shipped',        public total: number    ) {}

public addItem(productId: string, quantity: number, price: number): void { this.items.push({ productId, quantity, price }); this.total = this.calculateTotal(); }

private calculateTotal(): number { return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0); }}

export interface OrderItem { productId: string; quantity: number; price: number;}


Agora definimos a porta para o repositório:



// domain/ports/order.repository.interface.tsimport { Order } from '../order.entity';

export interface IOrderRepository { save(order: Order): Promise<void>; findById(id: string): Promise<Order | null>; findAll(): Promise<Order[]>;}


Passo 2. Caso de Uso — o coração da aplicação

O Caso de Uso é um cenário de uso. Ele contém a lógica de negócio e utiliza as portas, mas não sabe quem as implementa.



// application/use-cases/create-order.use-case.tsimport { Order } from '../../domain/order.entity';import { IOrderRepository } from '../../domain/ports/order.repository.interface';

export class CreateOrderUseCase { constructor(private readonly orderRepository: IOrderRepository) {}

async execute(use

Blogs

Book Recommendations