jonathanjuliani
Arquitetura Node errada: as 4 que vejo em todo projeto
Arquitetura Node errada tem nome: Controller Severino, God Service, Hexagonal de Cinema e NASA. Código real e quando cada padrão quebra em produção.
Você abre um projeto Node depois de três meses sem mexer. Vai trocar uma regra de negócio numa rota. E leva quatro horas pra achar onde aquela regra mora.
Você não tá burro. O projeto que tá errado.
O tipo de erro que esse projeto tem tem nome — na verdade, tem quatro. Quatro arquiteturas Node erradas que eu vejo em praticamente todo codebase que pego. É quase garantido que você trabalha com uma delas agora, ou já trabalhou. Neste post eu mostro as quatro com código de verdade, por que cada uma quebra em produção, e os três princípios que eu uso pra decidir quando separar lógica — sem receita de livro e sem over-engineering.
O que este post argumenta:
- Por que arquitetura ruim só dói depois que o projeto cresce
- O gap entre o que o tutorial ensina e o que o checkout real exige
- Os quatro anti-patterns com nome: Severino, God Service, Hexagonal de Cinema e NASA
- Três princípios práticos pra sair disso hoje
- Onde cada anti-pattern faz sentido — opinião sem nuance vira dogma
Quando arquitetura Node errada começa a custar caro
Arquitetura ruim não é problema enquanto o projeto é pequeno. Você sozinho, MVP, três rotas — tanto faz. Funciona.
O problema é quando:
- O time cresce e mais gente mexe no código
- A base passa de uns vinte arquivos
- Você precisa voltar depois de semanas sem tocar
- Algo quebra em produção e você tem cinco minutos pra achar
É aí que o que parecia "tá funcionando, deixa quieto" vira dia de incidente, três da manhã, cliente reclamando — e você não consegue isolar onde a lógica de pagamento mora.
Tutorial vs produção: onde nasce o Controller Severino
A maioria desses padrões erados não nasce de preguiça. Nasce de uma coisa simples: o tutorial te ensinou a fazer endpoint. Não te ensinou onde botar lógica.
Pega qualquer tutorial popular de Node — Express, Nest, Fastify, tanto faz. Ele te ensina a subir o servidor, criar rota, conectar no banco, retornar JSON. E termina. O dev sai sabendo fazer CRUD. Funciona.
Aí ele chega no mundo real e precisa fazer um checkout — validar carrinho, checar estoque, cobrar no Stripe, criar pedido, mandar email, atualizar ERP. Sete coisas, não uma.
E faz a única coisa que aprendeu: enfia tudo no controller. É aí que nasce o primeiro anti-pattern.
Controller Severino: o endpoint que faz sete coisas
Severino é o cara que faz tudo sozinho. Não delega. Coloca o pé na padaria, na lavanderia e no banco no mesmo dia. É exatamente isso que o teu controller virou.
@Post('/checkout')
async checkout(@Req() req) {
const body = req.body
if (!body.card) {
throw new BadRequestException()
}
const customer = await this.prisma.customer.findUnique(...)
const stock = await this.inventory.check(...)
const payment = await this.stripe.charge(...)
await this.prisma.order.create(...)
await this.mail.send(...)
return { success: true }
}Esse controller faz, numa única função:
- Validação de entrada
- Busca no banco
- Checagem de estoque
- Cobrança no Stripe
- Criação de pedido
- Envio de email
- Resposta HTTP
Sete responsabilidades. Como você testa isso? Você não testa. Você reza.
Onde mora a regra "só cobra se o estoque estiver disponível"? No meio. Misturada com erro HTTP, integração de pagamento e email.
"Tá Jon, mas e se for um endpoint simples? Tipo um login? Aí vale fazer assim, não vale?"
Vale. O problema não é o código do Severino. É deixar todo controller virar um Severino. Você move lógica do controller quando tiver mais de duas regras de negócio. Antes disso, é over-engineering. Depois disso, é dívida.
Se você acha que mover tudo pro service resolve — espera. O próximo é pior.
God Service: cinco dependências num createUser
O God Service nasce de boa intenção. O dev leu que "controller só orquestra, lógica vai no service". Beleza. Aí move tudo pro mesmo service.
@Injectable()
export class UserService {
constructor(
private prisma: PrismaService,
private mailService: MailService,
private stripeService: StripeService,
private analyticsService: AnalyticsService,
private cacheService: CacheService,
) {}
async createUser(dto: CreateUserDto) {
if (!dto.email.includes('@')) {
throw new Error('Invalid email');
}
const user = await this.prisma.user.create({ data: dto });
await this.stripeService.createCustomer(user);
await this.mailService.sendWelcomeEmail(user.email);
await this.analyticsService.track('USER_CREATED');
await this.cacheService.invalidate('users');
return user;
}
}Olha o construtor desse carinha. Cinco dependências. createUser faz seis coisas: validação, banco, Stripe, email, analytics, cache.
Esse não é um UserService. É um CreateUserOrchestrationService — só que se chama UserService, então cabe nele qualquer coisa com "user" no nome. Em três meses: mil linhas, dezesseis dependências, ninguém entende mais.
Quando o Stripe ficar lento, o que acontece com o cadastro?
Trava. O usuário espera oito segundos porque o Stripe tá em manutenção — porque você colocou criação de customer dentro da criação de usuário. Coisas que deviam ser separadas viraram uma coisa só.
"A mas eu achava que separar service por entidade era o jeito certo. UserService, OrderService, ProductService — não é assim?"
É um jeito. Não é "o" jeito. Em fluxos que orquestram integrações externas — email, pagamento, analytics — service por entidade é o atalho mais rápido pro God Service. A regra que eu uso: se o service tem mais de quatro dependências, ele virou orquestrador, não service.
Os dois primeiros são problema de onde a lógica mora. Os dois próximos são pior: problema de mentir sobre onde ela mora.
Hexagonal de Cinema: pasta domain/ com Prisma dentro
Essa é a do dev que leu o livro no fim de semana e voltou pra segunda inspirado.
A estrutura de pastas é linda:
src/
domain/
application/
infra/Você abre, vê isso, pensa "esse projeto é sério". Vai em entrevista, mostra a pasta, ganha pontos.
Aí abre domain/user.ts:
import { PrismaClient } from '@prisma/client';
import axios from 'axios';
export class User {
// ...
}A pasta domain é, na teoria, a parte que não conhece banco, HTTP nem nada externo — regra de negócio pura. A ideia é trocar de banco ou framework e o domain continua igual.
Daí você importa Prisma na primeira linha do domínio. E axios também, só pra garantir.
"Tá Jon, mas a arquitetura hexagonal não é boa?"
É excelente. Quando você usa de verdade. O problema não é a hexagonal. É cosplay de hexagonal. Pasta organizada não é arquitetura — é pasta organizada.
Sintoma simples: se domain/ importa framework, ORM ou cliente HTTP — sua hexagonal é de cinema. Fachada de loja fechada.
Eu prefiro um monolito honesto com src/ plano do que hexagonal de mentira. No monolito honesto ninguém te promete desacoplamento. A hexagonal de cinema promete — e entrega o acoplamento mais escondido do mundo, porque você acha que tá protegido.
Arquitetura NASA: sete interfaces num MVP
Você abre um MVP. POC. Coisa que devia estar no ar há dois meses.
meu-projeto-teste/
src/
domain/
application/
commands/
queries/
core/
shared/
common/
foundation/
abstractions/
contracts/
platform/
infrastructure/
database/
postgres/
IUserRepository.ts
IUserRepositoryAdapter.ts
IUserRepositoryFactory.ts
IUserRepositoryProvider.ts
IUserRepositoryManager.ts
IUserRepositoryRegistry.ts
IUserRepositorySingleton.tsSete interfaces pra repositório de usuário num MVP com três endpoints.
Você levou trinta minutos pra achar onde salva usuário no banco. Dentro do IUserRepository? Três métodos: find, create, update. A complexidade está toda nas abstrações, e a abstração não protege nada.
"Tá Jon, mas isso não tá pronto pra escalar?"
Escalar pra quê. Você tem zero usuários. Você não tá pronto pra escalar — tá pronto pra falir, porque gastou meses em abstração em vez de validar produto.
A NASA acontece quando o dev senior — bem-intencionado — monta estrutura pra empresa de mil pessoas num projeto de duas. Toda abstração criada antes de precisar é dívida, não investimento. A realidade nunca chega no formato que você previu.
Já trabalhei em projeto NASA. Você gasta mais tempo navegando do que escrevendo. O time fica lento. O produto evolui devagar. O concorrente num monolito feio te ultrapassa.
Os 3 princípios: quando separar lógica sem virar over-engineer
Não tem arquitetura certa pra todo projeto. Tem arquitetura certa pra cada momento. Os três princípios que eu uso:
1. Mova quando doer, não quando achar bonito
Lógica nasce no controller. Tá tudo bem. Você move pro service quando o controller tem mais de duas regras de negócio. Cria service novo quando o atual tem mais de quatro dependências. Cria camada de domínio quando — e só quando — tem regra que vale teste isolado.
Doeu? Move. Não doeu? Deixa quieto. Code review existe pra perceber quando dói.
2. Honestidade é melhor que estrutura
Prefiro src/ plano com vinte arquivos do que hexagonal de cinema. Prefiro controller Severino do que God Service que finge ser três services. A estrutura deve dizer o que o código faz — não mentir.
CRUD com algumas regras é CRUD. Não precisa chamar de Clean Architecture.
3. Abstração é resposta, não pergunta
Você não cria interface "pra caso de um dia mudar de banco". Cria quando mudou. Não cria repository pattern "pra desacoplar". Cria quando o acoplamento doeu.
Toda interface antes da dor é aposta. Você perde a maioria.
| Princípio | Regra prática |
|---|---|
| Mova quando doer | >2 regras no controller → service; >4 deps no service → dividir |
| Honestidade > estrutura | Nomeie o que o projeto é, não o que você quer que seja |
| Abstração responde | Interface só depois da dor real |
O contraponto que eu reconheço
Opinião sem nuance vira dogma. Cada anti-pattern acima tem terreno onde faz sentido:
| Anti-pattern | Onde faz sentido |
|---|---|
| Controller Severino | Endpoint com uma ou duas regras (login simples, health check) |
| God Service | CRUD fino, poucas integrações, time pequeno e prazo curto |
| Hexagonal real | Regras de negócio ricas, testáveis isoladas, múltiplos adapters de verdade |
| Abstrações "NASA" | Depois que a dor apareceu — troca de banco, segundo provider, compliance |
Isso não é hedging — é reconhecimento de limite. A tese continua: o erro mais comum não é escolher a arquitetura errada do catálogo. É aplicar arquitetura de escala enterprise no momento errado — ou fingir que já aplicou.
Na prática, quase ninguém tem só um anti-pattern. Hexagonal de Cinema convive com Severino. NASA convive com God Service. A pergunta útil não é "qual diagrama copiar", e sim quando começar a se importar — e quanto.
Próximos passos
Se o God Service te incomodou por causa de await em cadeia — Stripe, email, analytics no mesmo fluxo — vale revisar como JavaScript lida com assíncrono de verdade: Promises em JavaScript: do zero ao async/await.
E se a confusão é fronteira entre módulos, imports e o que fica exposto em cada arquivo, Node.js: require, exports e module.exports explicados ajuda a ver onde a separação começa antes de inventar dez pastas.
TL;DR
| Anti-pattern | Sintoma | Sinal de alerta | Saída prática |
|---|---|---|---|
| Controller Severino | Uma rota faz validação, banco, integração e HTTP | Mais de 2 regras de negócio no controller | Extrair service por fluxo |
| God Service | UserService com Stripe, mail, analytics e cache | Mais de 4 dependências no construtor | Orquestrador explícito ou fila/evento |
| Hexagonal de Cinema | Pastas lindas, domain/ importa Prisma/axios | Framework dentro de domain/ | Monolito honesto ou hexagonal de verdade |
| NASA | 7 interfaces pra 3 métodos de repositório | MVP com abstração de empresa grande | Abstrair só depois da dor |
Qual dos quatro você reconheceu no projeto agora — Severino, God Service, Hexagonal de Cinema ou NASA? Comenta sem vergonha; eu já trabalhei com os quatro. Se tem um quinto anti-pattern que merecia entrar na lista, ou se viu alguma coisa errada aqui, fala nos comentários — a gente troca.
Se quiser o detalhe técnico que não coube no post, a newsletter tá rolando junto com o canal — um insight por semana, direto no email.
Inscreva-se no Ledger da Engenharia
Receba notas de arquitetura e performance na sua caixa de entrada. Mesma lista do convite—inscreva-se aqui quando quiser.
Artigos relacionados
NodeJS - Entendendo de vez `require`,`exports` e `module.exports`
require, exports e module.exports no Node.js explicados com exemplos e erros comuns. Entenda CommonJS de vez, sem decorar.
Ler publicaçãoPromises em JavaScript: do zero ao async/await
Promises e async/await em JavaScript do zero: estados, encadeamento, tratamento de erros e os gotchas que a documentação oficial passa batido.
Ler publicação