Se você já sentiu que seu projeto ficou difícil de manter, testar ou evoluir com o tempo, provavelmente o problema não foi a linguagem ou o framework, mas sim a forma como a arquitetura foi pensada. É exatamente nesse ponto que o Design Pattern Hexagonal, também conhecido como Ports and Adapters, entra em cena. Ele é um padrão de arquitetura que ajuda a organizar o código de forma mais clara, desacoplada e preparada para mudanças — algo essencial no mundo real do desenvolvimento de software.
A ideia central da arquitetura hexagonal é simples: separar completamente a regra de negócio do restante do sistema, como banco de dados, APIs externas, filas, interfaces web ou mobile. Em vez de o domínio “conhecer” esses detalhes, tudo se comunica por meio de portas (interfaces) e adaptadores. Isso torna o sistema mais testável, mais flexível e muito mais fácil de evoluir.
Se você trabalha com Node.js e NestJS, boas notícias: o NestJS se encaixa muito bem com o conceito de arquitetura hexagonal, principalmente por incentivar o uso de módulos, injeção de dependência e interfaces. Ao longo deste artigo, vamos ver exemplos práticos de como aplicar o padrão hexagonal usando NestJS, sempre com uma abordagem clara e amigável para quem está começando.
Nosso objetivo é justamente descomplicar conceitos que parecem avançados à primeira vista. Não importa se você é iniciante ou já desenvolve há algum tempo: entender o Design Pattern Hexagonal pode mudar completamente a forma como você estrutura seus projetos e encara a manutenção do código no dia a dia.
O que é o Design Pattern Hexagonal na prática?
Antes de pensar em código, frameworks ou pastas, é importante entender a ideia por trás do Design Pattern Hexagonal. Diferente de muitos padrões que focam em resolver problemas específicos de código, a arquitetura hexagonal olha para o sistema como um todo e propõe uma forma mais saudável de organizar responsabilidades.
Na prática, o padrão hexagonal diz o seguinte:
o coração da aplicação deve ser o domínio, ou seja, as regras de negócio. Tudo o que é externo — banco de dados, API REST, filas, serviços de terceiros, interface web ou mobile — deve ficar “ao redor” desse núcleo, sem acoplamento direto.
O nome “hexagonal” não é sobre a forma do código, mas sim uma metáfora visual. Imagine o domínio no centro e, ao redor, vários lados (ou portas) por onde o sistema se comunica com o mundo externo. Cada lado representa uma porta, que é basicamente uma interface. Quem implementa essa interface é um adaptador.
Portas e Adaptadores, sem complicação
- Portas: são contratos (interfaces) que definem como o domínio se comunica.
Exemplo: uma interface para salvar um usuário, buscar pedidos ou enviar uma notificação. - Adaptadores: são implementações dessas portas.
Exemplo: um adaptador que usa PostgreSQL, outro que usa MongoDB, ou até um mock para testes.
O ponto mais importante aqui é que o domínio não sabe quem está do outro lado. Ele apenas conhece a interface. Isso significa que você pode trocar o banco de dados, mudar de REST para GraphQL ou até alterar o framework sem precisar reescrever suas regras de negócio.
Por que isso é tão poderoso?
Em projetos tradicionais, é comum ver regras de negócio misturadas com chamadas de banco, validações de HTTP e detalhes de framework. Isso funciona no começo, mas vira um problema com o tempo. Já com a arquitetura hexagonal:
- O código fica mais organizado e legível
- Testar regras de negócio se torna muito mais simples
- Mudanças externas não quebram o coração da aplicação
- O sistema cresce de forma mais controlada
No contexto do NestJS, esse padrão faz ainda mais sentido, porque o framework já incentiva boas práticas como injeção de dependência e separação de camadas.
Principais componentes da Arquitetura Hexagonal
Agora que a ideia geral já está clara, vamos entender quais são os componentes que formam a arquitetura hexagonal e qual é o papel de cada um. Não se preocupe com termos difíceis — a proposta aqui é manter tudo simples e prático, do jeito que usamos no dia a dia em projetos reais.
1. Domínio (o coração da aplicação)
O domínio é a parte mais importante do sistema. É aqui que ficam as regras de negócio, totalmente independentes de framework, banco de dados ou protocolo de comunicação.
No domínio você normalmente encontra:
- Entidades (User, Order, Product, etc.)
- Value Objects
- Regras e validações de negócio
- Interfaces (portas)
O domínio não deve importar nada do NestJS, nem de ORM, nem de HTTP. Ele precisa ser puro. Isso garante que ele possa ser testado facilmente e reaproveitado se necessário.
2. Portas (interfaces)
As portas são contratos que o domínio define para se comunicar com o mundo externo. Elas dizem o que precisa ser feito, mas não como.
Exemplos de portas:
- UserRepository
- PaymentService
- NotificationGateway
Essas interfaces ficam próximas do domínio, pois são parte da regra de negócio, não da infraestrutura.
3. Adaptadores (infraestrutura)
Os adaptadores são responsáveis por implementar as portas. Aqui entram:
- Banco de dados (PostgreSQL, MongoDB, Prisma, TypeORM)
- APIs externas
- Filas (RabbitMQ, SQS)
- Controllers HTTP
Eles conhecem o domínio, mas o domínio não conhece eles. Essa inversão de dependência é um dos pilares do padrão hexagonal.
4. Aplicação (casos de uso)
A camada de aplicação conecta tudo. Ela orquestra os casos de uso, chamando entidades e portas do domínio. Não é regra de negócio pura, mas também não é infraestrutura.
Exemplo de caso de uso:
- Criar usuário
- Processar pagamento
- Cancelar pedido
No NestJS, essa camada costuma se encaixar muito bem nos services, desde que eles não fiquem cheios de lógica de infraestrutura.
Como tudo se conecta?
O fluxo geralmente funciona assim:
Controller → Caso de uso → Domínio → Porta → Adaptador
Essa separação deixa o sistema mais previsível, organizado e fácil de manter.
Estrutura de pastas no NestJS usando Arquitetura Hexagonal
Uma das dúvidas mais comuns quando alguém começa a estudar arquitetura hexagonal é: “onde eu coloco cada coisa no projeto?”. A boa notícia é que não existe uma única estrutura perfeita, mas existem organizações que funcionam muito bem, especialmente com NestJS.
Aqui a ideia não é criar algo engessado, e sim uma estrutura clara, fácil de entender e de escalar.
Uma estrutura simples e funcional
Abaixo está um exemplo de organização baseada em domínio, bem alinhada com o padrão hexagonal:
src/
├── domain/
│ ├── entities/
│ │ └── user.entity.ts
│ ├── ports/
│ │ └── user-repository.port.ts
│ └── use-cases/
│ └── create-user.usecase.ts
│
├── application/
│ └── services/
│ └── create-user.service.ts
│
├── infrastructure/
│ ├── database/
│ │ └── user.repository.ts
│ ├── http/
│ │ └── user.controller.ts
│ └── modules/
│ └── user.module.ts
│
└── main.ts
Vamos entender isso por partes.
Domain: regras de negócio puras
Tudo o que está dentro de domain não conhece o NestJS. Aqui ficam as entidades, portas e casos de uso. Se você quisesse reaproveitar essa lógica em outro projeto ou até em outra linguagem, esse seria o ponto de partida.
Application: orquestração
A camada de application serve como uma ponte entre o domínio e a infraestrutura. Ela coordena os casos de uso, aplicando regras e chamando as portas necessárias.
Em projetos menores, essa camada pode até ser mesclada com o domínio, mas separá-la ajuda muito quando o sistema cresce.
Infrastructure: detalhes técnicos
Aqui entram os detalhes que mudam com mais frequência:
- Controllers (REST, GraphQL)
- Implementações de repositórios
- ORM
- Integrações externas
Essas classes implementam as interfaces definidas no domínio.
E o NestJS entra onde?
O NestJS fica principalmente na infraestrutura, conectando tudo por meio da injeção de dependência. Ele sabe qual adaptador usar para cada porta, mas o domínio continua isolado.
Exemplo prático: começando pelo domínio no NestJS
Agora vamos sair um pouco da teoria e partir para um exemplo prático, do jeito que acontece no dia a dia. A melhor forma de aplicar a arquitetura hexagonal é sempre começar pelo domínio, sem pensar ainda em controller, banco de dados ou framework.
Vamos imaginar um caso simples: criação de usuários.
Criando a entidade User
No domínio, a entidade representa o conceito central do negócio. Ela contém dados e regras que fazem sentido para o problema, não para o banco ou para a API.
// src/domain/entities/user.entity.ts
export class User {
constructor(
public readonly id: string,
public name: string,
public email: string,
) {
if (!email.includes('@')) {
throw new Error('Email inválido');
}
}
}
Perceba que:
- Não existe decorator do NestJS
- Não existe ORM
- A validação faz parte da regra de negócio
Criando a porta (interface do repositório)
Agora o domínio precisa salvar esse usuário em algum lugar. Mas ele não quer saber onde. Por isso criamos uma porta.
// src/domain/ports/user-repository.port.ts
import { User } from '../entities/user.entity';
export interface UserRepository {
save(user: User): Promise<void>;
findByEmail(email: string): Promise<User | null>;
}
Essa interface define o contrato. O domínio só conhece isso.
Criando o caso de uso
O caso de uso é quem orquestra a regra. Ele recebe dados, aplica validações e usa as portas.
// src/domain/use-cases/create-user.usecase.ts
import { UserRepository } from '../ports/user-repository.port';
import { User } from '../entities/user.entity';
import { randomUUID } from 'crypto';
export class CreateUserUseCase {
constructor(private readonly userRepository: UserRepository) {}
async execute(name: string, email: string): Promise<User> {
const existingUser = await this.userRepository.findByEmail(email);
if (existingUser) {
throw new Error('Usuário já existe');
}
const user = new User(randomUUID(), name, email);
await this.userRepository.save(user);
return user;
}
}
Note como esse código:
- Não depende de banco de dados
- Não depende de HTTP
- Pode ser testado com mocks facilmente
Até aqui, nenhum NestJS apareceu, e isso é proposital. Essa é a essência da arquitetura hexagonal.
Implementando os adaptadores na infraestrutura com NestJS
Agora que o domínio está bem definido, chegou a hora de conectar o mundo externo a ele. É aqui que entram os adaptadores, que vivem na camada de infraestrutura e conhecem tanto o NestJS quanto as portas definidas no domínio.
Vamos continuar com o exemplo de criação de usuários.
Implementando o repositório (adaptador de saída)
Esse adaptador é responsável por falar com o banco de dados. Para simplificar, vamos usar um repositório em memória, mas a ideia é exatamente a mesma para Prisma, TypeORM ou qualquer outro ORM.
// src/infrastructure/database/user.repository.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from '../../domain/ports/user-repository.port';
import { User } from '../../domain/entities/user.entity';
@Injectable()
export class InMemoryUserRepository implements UserRepository {
private users: User[] = [];
async save(user: User): Promise<void> {
this.users.push(user);
}
async findByEmail(email: string): Promise<User | null> {
return this.users.find(user => user.email === email) || null;
}
}
Aqui sim:
- Usamos
@Injectable() - Implementamos a porta do domínio
- O domínio continua sem saber que isso existe
Criando o Controller (adaptador de entrada)
O controller é outro tipo de adaptador. Ele traduz uma requisição HTTP para um formato que o domínio entende.
// src/infrastructure/http/user.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { CreateUserUseCase } from '../../domain/use-cases/create-user.usecase';
@Controller('users')
export class UserController {
constructor(
private readonly createUserUseCase: CreateUserUseCase,
) {}
@Post()
async create(@Body() body: { name: string; email: string }) {
return this.createUserUseCase.execute(body.name, body.email);
}
}
O controller:
- Não tem regra de negócio
- Apenas repassa os dados
- Traduz HTTP para o caso de uso
Conectando tudo no módulo
O NestJS é responsável por “colar” as peças usando injeção de dependência.
// src/infrastructure/modules/user.module.ts
import { Module } from '@nestjs/common';
import { UserController } from '../http/user.controller';
import { InMemoryUserRepository } from '../database/user.repository';
import { CreateUserUseCase } from '../../domain/use-cases/create-user.usecase';
@Module({
controllers: [UserController],
providers: [
CreateUserUseCase,
{
provide: 'UserRepository',
useClass: InMemoryUserRepository,
},
],
})
export class UserModule {}
Em projetos reais, você pode usar tokens, símbolos ou classes abstratas para representar as portas.
Vantagens e desafios da Arquitetura Hexagonal em projetos reais
Depois de ver a teoria e um exemplo prático com NestJS, é natural surgir a pergunta: vale mesmo a pena usar arquitetura hexagonal no dia a dia? A resposta curta é: depende do contexto. Como quase tudo em software, existem vantagens claras, mas também alguns desafios que precisam ser considerados.
Principais vantagens
A primeira grande vantagem é o baixo acoplamento. Como o domínio não depende de detalhes externos, trocar um banco de dados, mudar um ORM ou até substituir uma API REST por GraphQL se torna muito mais simples. Isso reduz bastante o medo de mudanças no projeto.
Outro ponto forte é a testabilidade. Como as regras de negócio estão isoladas, você consegue testar casos de uso sem subir servidor, sem banco de dados e sem mocks complexos de framework. Isso melhora a qualidade do código e acelera o feedback durante o desenvolvimento.
A clareza de responsabilidades também é um destaque. Cada parte do sistema tem um papel bem definido:
- Domínio cuida das regras
- Aplicação orquestra
- Infraestrutura resolve detalhes técnicos
Em times, isso ajuda muito na comunicação e na manutenção do código ao longo do tempo.
Principais desafios
O principal desafio para quem está começando é a complexidade inicial. Para projetos muito pequenos, a arquitetura hexagonal pode parecer exagerada. Criar interfaces, casos de uso e adaptadores exige disciplina e entendimento do problema.
Outro ponto é a curva de aprendizado. Desenvolvedores iniciantes podem estranhar o fato de o controller não ter lógica nenhuma ou o domínio não conhecer o banco de dados. Isso é normal e faz parte do processo.
Além disso, se o time não seguir o padrão corretamente, existe o risco de misturar responsabilidades e perder os benefícios da arquitetura.
Quando usar?
A arquitetura hexagonal brilha em:
- Sistemas que vão crescer
- Projetos com regras de negócio complexas
- Aplicações que precisam mudar com frequência
Conclusão
A arquitetura hexagonal não é uma moda nem uma regra obrigatória, mas sim uma ferramenta poderosa para quem quer construir software mais organizado, testável e preparado para mudanças. Ao longo deste artigo, vimos que a ideia central é simples: proteger o domínio e evitar que as regras de negócio fiquem reféns de detalhes técnicos como frameworks, bancos de dados ou protocolos de comunicação.
Usando NestJS, esse padrão se encaixa de forma muito natural. A injeção de dependência, a separação em módulos e o incentivo ao uso de boas práticas tornam a aplicação da arquitetura hexagonal muito mais tranquila do que parece à primeira vista. Começando pelo domínio, criando portas bem definidas e implementando adaptadores claros, você ganha um sistema mais previsível e fácil de manter.
Claro, nem todo projeto precisa nascer 100% hexagonal. Em aplicações menores, você pode aplicar os conceitos aos poucos: separar melhor as regras de negócio, evitar lógica nos controllers e usar interfaces para reduzir acoplamento. Com o tempo, essa mentalidade já faz uma enorme diferença na qualidade do código.
Aqui no CulturaDev, a ideia é justamente essa: ajudar você a evoluir como desenvolvedor, entendendo não só como escrever código, mas por que certas decisões arquiteturais fazem sentido. A arquitetura hexagonal é um ótimo próximo passo para quem já domina o básico e quer subir o nível profissional.
Agora quero saber de você: você já tentou aplicar a arquitetura hexagonal em algum projeto? Teve dificuldades ou viu benefícios claros? Deixe seu comentário abaixo e compartilhe sua experiência. Essa troca é o que faz a comunidade crescer.