Wrong Node.js architecture: 4 anti-patterns I see in every codebase
Wrong Node.js architecture has a name: Controller Severino, God Service, Hexagonal Theater, and NASA. Real code and when each pattern breaks in production.
Jonathan Juliani
Sr Software Engineer - Full Stack · WithClutch Inc. | Juliani Tecnologia
Node.jsTypeScriptArchitectureBackend
Audio in Brazilian Portuguese · YouTube offers automatic captions and translation
You open a Node project after three months away. You need to change one business rule in a route. Four hours later, you still have not found where that rule lives.
You are not slow. The project is wrong.
That kind of mess has a name — four names, actually. Four wrong Node.js architecture patterns I see in almost every codebase I touch. You are probably running one of them right now, or you have before. This post walks through all four with real code, why each one breaks in production, and three principles I use to decide when to split logic — without a textbook recipe or premature abstraction.
What this post argues:
Why bad architecture only hurts after the project grows
The gap between what tutorials teach and what a real checkout needs
Four named anti-patterns: Severino, God Service, Hexagonal Theater, and NASA
Three practical principles to fix the habit today
Where each anti-pattern still makes sense — opinion without nuance becomes dogma
When wrong Node.js architecture starts to hurt
Bad architecture is fine while the project is small. Solo dev, MVP, three routes — it works.
It hurts when:
The team grows and more people touch the code
The codebase passes roughly twenty files
You come back after weeks away
Production breaks and you have five minutes to find the bug
That is when "it works, leave it" turns into an incident at 3 a.m. — and you cannot isolate where payment logic lives.
Tutorials vs production: where the fat controller comes from
Most of these patterns are not laziness. They come from one gap:
Subscribe to the Engineering Ledger
Get architecture and performance notes in your inbox. Same list as the timed prompt—subscribe here anytime.
Wrong Node.js architecture: 4 anti-patterns I see in every codebase | jonathanjuliani.dev
tutorials teach endpoints. They do not teach where logic belongs.
Pick any popular Node tutorial — Express, Nest, Fastify. It shows how to boot the server, add a route, connect a database, return JSON. Then it ends. You can ship a CRUD. It works.
Then you need a real checkout — validate the cart, check stock, charge Stripe, create the order, send email, update the ERP. Seven concerns, not one.
So you do the only thing you were taught: put everything in the controller. That is anti-pattern number one.
Controller Severino: one endpoint, seven jobs
Severino does everything himself. No delegation. Bakery, laundry, and bank in one afternoon. That is your controller.
Where does "only charge if stock is available" live? In the middle — mixed with HTTP errors, payment integration, and mail.
But what about a simple login endpoint? Fair point — that pattern is fine there. The problem is not Severino code. It is letting every controller become Severino. Move logic out when you have more than two business rules in the handler. Before that, splitting is over-engineering. After that, it is debt.
If you think moving everything to a service fixes it — wait. The next one is worse.
God Service: five dependencies in createUser
The God Service starts with good intent. You read that controllers should only orchestrate and logic belongs in services. Fine. Then you move everything into the same service.
Five constructor dependencies. createUser does six things: validation, database, Stripe, email, analytics, cache.
This is not a UserService. It is a CreateUserOrchestrationService wearing the wrong name — so anything "user-related" lands in the same file. Three months later: a thousand lines, sixteen dependencies, no owner.
When Stripe is slow, what happens to signup?
It blocks. Users wait eight seconds because Stripe is in maintenance — because customer creation runs inside user creation. Separate concerns became one synchronous chain.
You might have heard that one service per entity is the right model: UserService, OrderService, ProductService. That is a model, not the model. For flows that orchestrate external systems — mail, payments, analytics — entity services are the fastest path to a God Service. My rule: more than four dependencies means orchestrator, not domain service.
The first two patterns are about where logic lives. The next two are worse: lying about where it lives.
Hexagonal Theater: a domain/ folder that imports Prisma
This one is the dev who read the book over the weekend and came back Monday inspired.
The folder structure looks great:
code
src/ domain/ application/ infra/
You open the repo, think "serious project," show it in an interview.
Then you open domain/user.ts:
code
import { PrismaClient } from '@prisma/client';import axios from 'axios';export class User { // ...}
In theory, domain should not know about databases, HTTP, or external clients — pure business rules. You should swap the database or framework and keep domain stable.
Then Prisma is line one of the domain file. Axios too, just in case.
Is hexagonal architecture good? Yes — when you actually use it. The problem is not hexagonal. It is hexagonal cosplay. Organized folders are not architecture.
Simple symptom: if domain/ imports a framework, ORM, or HTTP client, you have theater — a closed storefront with a pretty facade.
I would rather ship an honest flat src/ monolith than fake hexagonal. The honest monolith does not promise decoupling. Theater promises it and delivers the worst coupling: hidden, because you think you are safe.
NASA architecture: seven interfaces for an MVP
You open an MVP. A POC that should have shipped two months ago.
Seven interfaces for a user repository in an MVP with three endpoints.
Thirty minutes to find where users are saved. Inside IUserRepository? Three methods: find, create, update. All the complexity is in abstractions that protect nothing.
"But is it not ready to scale?" Scale to what. You have zero users. You are not ready to scale — you are ready to run out of runway because you spent months on abstraction instead of validating the product.
NASA architecture is what happens when a well-meaning senior builds for a thousand-person company in a two-person project. Every abstraction you add before you need it is debt, not investment. Reality never matches the shape you predicted.
I have worked in NASA projects. You navigate more than you build. The team slows. The product slows. A competitor on an ugly monolith passes you.
Three principles: when to split without over-engineering
There is no one right architecture for every project. There is a right fit for each stage. Three principles I use:
1. Move when it hurts, not when it looks impressive
Logic can start in the controller. That is fine. Move to a service when the controller has more than two business rules. Split the service when it has more than four dependencies. Add a domain layer only when you have rules worth testing in isolation.
Hurts? Move. Does not hurt? Leave it. Code review exists to catch when it starts hurting.
2. Honesty beats structure
I prefer a flat src/ with twenty files over theater folders. I prefer an honest fat controller over a God Service pretending to be three services. Structure should describe what the code does.
A CRUD with a few rules is a CRUD. It does not need to be called Clean Architecture.
3. Abstraction is an answer, not a question
You do not add an interface "in case we change databases someday." You add it when you changed. You do not add a repository "to decouple." You add it when coupling hurt.
Every interface before pain is a bet. You lose most of them.
Principle
Practical rule
Move when it hurts
>2 rules in controller → service; >4 deps in service → split
Honesty > structure
Name what the project is, not what you wish it were
Abstraction answers
Interfaces only after real pain
The counterpoint I accept
Strong opinions need limits. Each anti-pattern has ground where it fits:
Anti-pattern
Where it fits
Controller Severino
One or two rules per endpoint (simple login, health check)
God Service
Thin CRUD, few integrations, small team and tight deadline
Real hexagonal
Rich business rules, real isolated tests, multiple real adapters
"NASA" abstractions
After pain shows up — database swap, second provider, compliance
That is not weakening the thesis — it is naming boundaries. The common mistake is not picking the wrong diagram from a catalog. It is applying enterprise-scale structure at the wrong time — or pretending you already did.
In practice, almost nobody has just one anti-pattern. Theater hexagonal lives with Severino. NASA lives with God Service. The useful question is not which diagram to copy, but when to care — and how much.
Next steps
If the God Service bothered you because of chained await — Stripe, email, analytics in one flow — review how JavaScript actually handles async: Promises in JavaScript: from zero to async/await.
Which of the four shows up in your codebase right now — Severino, God Service, Hexagonal Theater, or NASA? Drop it in the comments. If you have a fifth anti-pattern that belongs on this list, or something here is off, say so — I want the correction.
For the technical detail that does not fit a single post, the newsletter runs alongside the channel — one insight per week in your inbox.