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.

1 Comments

  • Ali Atazadeh
    09 Jun, 2026

    Im about to start my own project and this blog is exactly what I was looking for.

Leave a Comment