Problems with Bad Architecture: Stop Writing Spaghetti Code in Node.js (Beginner-Friendly Guide)

March 10, 2026 Β· 7 min read

Hey there! πŸ‘‹

If you're a beginner or intermediate developer building Node.js/Express apps, you've probably written code like this:

app.post("/users", async (req, res) => {
  const user = await db.collection("users").insertOne({
    email: req.body.email,
    password: req.body.password
  });

  res.json(user);
});

Looks simple, right? But tomorrow if you want to:

…everything breaks. You end up touching the same file over and over. That's spaghetti code β€” everything tangled together.

This is where Clean Architecture saves the day.

Remember the Hospital Visit? πŸ₯

You don't walk straight to the lab technician or pharmacy counter when you go for treatment.

The flow is:

You (patient) β†’ Reception β†’ Doctor β†’ (maybe) Lab β†’ (maybe) Pharmacy

Each person has one clear job:

If the pharmacy upgrades its billing software or is under renovation? The doctor's job stays exactly the same.

That's Clean Architecture in one sentence: Protect your core business rules so infrastructure changes don't break everything.

The Clean Architecture Layers (The Onion πŸ§…)

Uncle Bob (Robert C. Martin) drew it like concentric circles β€” dependencies always point inward.

Clean Architecture onion diagram by Uncle Bob

Classic Clean Architecture diagram (source: blog.cleancoder.com)

For a simple Node.js app, we simplify it to:

  1. Controllers (outermost) β†’ Handle HTTP, requests/responses
  2. Use Cases β†’ The brain β€” business rules & logic
  3. Repositories β†’ Data access (abstracts the database)
  4. Database / External β†’ Actual MongoDB, PostgreSQL, etc.

Arrows point inward β†’ Inner layers know nothing about outer layers.

Let's Break Down Each Layer with Real Code

1. Controller (API Layer) β€” Just the Doorway πŸšͺ

Only cares about HTTP.

// controllers/user.controller.js
async function registerUser(req, res) {
  try {
    const userData = req.body;
    const newUser = await registerUserUseCase.execute(userData);
    res.status(201).json(newUser);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
}

2. Use Case (Business Logic) β€” The Doctor 🩺

This is where real decisions happen.

// usecases/registerUser.js
class RegisterUser {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  async execute(data) {
    if (!data.email) {
      throw new Error("Email is required");
    }
    if (!data.password || data.password.length < 8) {
      throw new Error("Password must be at least 8 characters");
    }

    // Could add: check if email already exists, hash password, etc.
    const user = { email: data.email, /* hashed password later */ };
    return await this.userRepository.create(user);
  }
}

3. Repository (Data Access) β€” The Tools πŸ”§

Hides how data is stored.

// repositories/user.repository.js
class UserRepository {
  async create(userData) {
    // This is the ONLY place that knows about MongoDB
    const result = await db.collection("users").insertOne(userData);
    return { id: result.insertedId, ...userData };
  }

  // Later you can swap to:
  // async create(userData) { return prisma.user.create({ data: userData }); }
}

Change database? Only touch this file.

Folder Structure Clean Architects Love πŸ“‚

src/
β”œβ”€β”€ controllers/
β”‚   └── user.controller.js
β”œβ”€β”€ usecases/
β”‚   └── registerUser.js
β”œβ”€β”€ repositories/
β”‚   └── user.repository.js
β”œβ”€β”€ entities/          (optional: domain models)
β”‚   └── user.js
└── config/            (DB connection, etc.)
    └── database.js

Clean separation = easy to test, easy to change.

Node.js Clean Architecture folder structure example - feature-based use cases

Feature-based folder structure example (inspired by Khalil Stemmler)

The Golden Rule ⭐ (Never Forget This)

Inner layers DO NOT depend on outer layers.

Dependencies point inward β€” that's why changes in outer layers (framework, DB, UI) don't break the core.

Quick Test: Did You Get It? πŸ˜„

Question: Tomorrow we switch from MongoDB β†’ PostgreSQL (or add caching, or go serverless). How many files should change?

Correct answer: Only the repository layer (maybe config too).

If you said "controllers" or "use cases" β†’ go back and re-read the golden rule! πŸ˜‰

One-Line Summary

Clean Architecture = separating responsibilities so that changing databases, frameworks, or APIs doesn't force you to rewrite your entire business logic.

Your app becomes:

Ready to refactor your messy Express app? Start small β€” extract one endpoint into controller + use case + repository. You'll feel the difference immediately.

What We Will Learn Next

In the upcoming sections of this series we will explore: