Architecture That Doesn't Collapse: How I Structure Scalable Node.js, React, and Next.js Applications
If you've ever come back to a codebase after 3–6 months and felt mild panic... congratulations, you've experienced software entropy.
It's that slow, silent decay where everything still works, but nobody is fully sure why.
I've seen it happen in React apps, Express backends, Next.js projects, startups with five developers, and enterprise teams with fifty. Different stacks. Same outcome.
The apps that survive growth are rarely the ones using the newest framework or the most sophisticated architecture pattern. They're usually the ones that made boring, predictable structure decisions early and enforced them consistently.
Over the years I've ended up converging on the same philosophy regardless of whether I'm building a Node.js backend, a React SPA, or a Next.js application:
Organize around features, enforce boundaries, and make ownership obvious.
Here's what that looks like.
1. The Core Idea: Stop Organizing by Technical Type
Most projects begin with folders like this:
components/
hooks/
services/
utils/
controllers/
repositories/
It feels clean.
Until six months later.
Now a single feature touches half the repository. Refactoring becomes a scavenger hunt. Deleting a feature feels like performing archaeology on a lost civilization.
Instead, I structure applications around feature boundaries.
Whether it's backend or frontend, the principle stays the same:
modules/
auth/
questions/
billing/
notifications/
shared/
A feature owns everything it needs.
UI, API communication, business logic, validation, types, tests, configuration.
When somebody asks:
"Where does the lawyer booking feature live?"
There should be exactly one answer.
2. Express.js: Business Logic Deserves a Home
The most common Express architecture I see is:
routes/
controllers/
services/
Looks organized.
Then the project grows.
Controllers start containing business logic.
Database queries appear everywhere.
Validation gets duplicated.
Nobody knows which layer actually owns anything.
Instead, I keep modules self-contained:
src/
modules/
auth/
auth.model.ts
auth.repository.ts
auth.service.ts
auth.controller.ts
auth.route.ts
questions/
lawyers/
shared/
Every file has one responsibility.
Repository
Database access only.
Service
Business rules only.
Controller
HTTP request handling only.
Route
Wiring only.
The moment Prisma queries start appearing inside controllers, the architecture is already drifting.
A good rule:
If I can replace Express tomorrow and keep most of the business logic untouched, the boundaries are probably healthy.
3. React SPAs: Features Own Their UI
React projects often start clean and slowly become impossible to navigate.
The usual structure:
components/
hooks/
pages/
services/
Eventually every feature becomes spread across five different directories.
Instead:
src/
modules/
auth/
components/
hooks/
services/
types/
Auth.page.tsx
questions/
components/
hooks/
services/
types/
Questions.page.tsx
shared/
Everything related to authentication lives inside the auth module.
Everything related to questions lives inside the questions module.
Deleting a feature becomes deleting a folder.
Refactoring a feature becomes opening one folder.
That's a huge quality-of-life improvement once the codebase crosses a few hundred files.
4. Next.js: The App Router Is Not Your Architecture
This is probably my biggest Next.js hot take.
Many developers treat the App Router as their application architecture.
I think that's a mistake.
The app directory should primarily exist for routing.
Nothing more.
I prefer:
app/
questions/page.tsx
lawyers/page.tsx
modules/
questions/
lawyers/
consultations/
The route file becomes an entry point:
import { QuestionsPage } from "@/modules/questions/Questions.page";
export default function Page() {
return <QuestionsPage />;
}
That's it.
No business logic.
No API logic.
No giant page components.
No mysterious helper functions.
The route exists to connect the URL to the feature.
The feature contains the application.
This keeps Next.js-specific concerns isolated and makes future framework changes dramatically easier.
5. Co-Location Is Survival, Not Preference
A component usually needs:
-
Types
-
Validation
-
Hooks
-
API communication
-
Tests
Those pieces should live near each other.
Not scattered around the repository like Easter eggs.
For example:
modules/questions/
components/
hooks/
services/
schemas/
types/
tests/
This isn't just about organization.
It's about reducing the number of places your brain must search before making a change.
The fewer folders you need to inspect, the faster development becomes.
6. API Boundaries Matter More Than Framework Choice
One of the fastest ways to create chaos is letting components talk directly to HTTP.
I've seen React components with embedded fetch calls.
I've seen Express services calling third-party APIs directly from random utility files.
I've seen enough to know where this story ends.
Instead:
modules/questions/services/questions.service.ts
The UI talks to a service.
The service talks to the API.
The API talks to the backend.
Each layer has a clear responsibility.
This gives you one place for:
-
Authentication
-
Error normalization
-
Retry logic
-
Response transformation
-
Logging
And when APIs inevitably change, you update one layer instead of fifty components.
7. Shared Folders Are Where Architectures Go To Die
Every project eventually gets a shared folder.
The danger is turning it into a junk drawer.
I usually keep strict categories:
shared/
components/
hooks/
services/
types/
utils/
And one rule:
If only one feature uses it, it does not belong in shared.
Most shared folders become giant dumping grounds because nobody enforces ownership.
A shared folder should contain reusable contracts.
Not abandoned code.
8. TypeScript Is an Architectural Tool
A lot of developers see TypeScript as a bug-prevention tool.
I see it as an architecture enforcement tool.
I rely heavily on:
-
Strict mode
-
Feature-owned types
-
Explicit API contracts
-
Shared domain models
If types are leaking across module boundaries, architecture is usually leaking too.
Good TypeScript forces you to think about ownership.
And ownership is what scalable systems are built on.
9. Folder Structure Isn't Architecture
This is the part people usually miss.
You can copy the perfect folder structure from GitHub and still end up with a mess.
Architecture is really about constraints.
Some of the rules I enforce:
-
No cross-feature imports without explicit justification
-
No database access outside repositories
-
No API calls inside UI components
-
No business logic inside routes or controllers
-
No feature-specific code inside shared
Without enforcement, every architecture eventually collapses.
The folder structure just makes the rules visible.
10. The Real Goal: Reduce Cognitive Load
When I join a project, I want to answer three questions immediately:
-
Where does this feature live?
-
Where does this logic belong?
-
What breaks if I delete this folder?
If those answers are obvious, the architecture is doing its job.
If they require ten minutes of detective work, technical debt is already collecting interest.
Closing Thoughts
The best architectures I've worked with weren't the most clever.
They were the most predictable.
Whether it's Node.js, React, or Next.js, the winning pattern is almost always the same:
-
Strong feature boundaries
-
Clear ownership
-
Co-located code
-
Minimal ambiguity
Complexity doesn't come from frameworks.
Complexity comes from a lack of constraints.
The boring architectures are usually the ones that survive.
Leave a Comment