Introduction

One of the most paralyzing moments in a new project occurs right after you type mkdir my-app. You stare at the empty directory and ask: "How should I organize this?"

Should you create a folder for `controllers` and `models`? Or should you create folders for `users` and `products`? Should utility functions go in `lib`, `utils`, `helpers`, or `common`?

In my experience, developers (myself included) spend way too much time obsessing over the "perfect" architecture before writing a single line of business logic. We over-engineer folder structures for a future complexity that might never arrive.

The truth is, there is no perfect structure. However, there are pragmatic principles that allow your codebase to grow organically without turning into a mess. In this guide, we will explore strategies to split code effectively, focusing on readability and cohesion rather than rigid dogma.

The Trap of "Layer-Based" Grouping

Traditionally, many frameworks (like MVC frameworks) encouraged grouping files by technical role.

// ❌ The "Sock Drawer" Approach (Grouping by Type)
/src
  /controllers
    UserController.js
    ProductController.js
    OrderController.js
  /models
    User.js
    Product.js
    Order.js
  /views
    UserView.js
    ProductView.js
    OrderView.js

This looks neat, like sorting socks into one drawer and shirts into another. But software isn't laundry. Software is about features.

The Problem: To work on the "User Registration" feature, you have to jump between three different folders. You are constantly scrolling up and down the file tree. This high cognitive load makes it hard to see the feature as a cohesive unit.

The Solution: Feature-Based Grouping

Instead of grouping by what the file is (a controller), group by what the file does (User logic). This is often called "Colocation" or "Vertical Slicing."

// ✅ The "Feature" Approach (Grouping by Domain)
/src
  /features
    /users
      UserController.js
      User.js
      UserView.js
    /products
      ProductController.js
      Product.js
      ProductView.js
    /orders
      OrderController.js
      Order.js
      OrderView.js

Why this wins:

When to Extract a File

Start simple. It is perfectly fine to have one large file named `Users.js` initially. Do not split it until you feel the pain.

The Rule of 300 Lines:
In my experience, files under 300 lines are easy to read. Once a file crosses 300-500 lines, it becomes hard to hold the mental model in your head. That is the trigger to refactor.

How to split:
Don't just split it arbitrarily (e.g., `UsersPart1.js`). Split by responsibility.

// Before: Giant User.js
class User {
    login() { ... }
    register() { ... }
    validatePassword() { ... } // Complicated regex logic
    formatAddress() { ... }    // Complicated string logic
    updateAvatar() { ... }     // Image processing logic
}

// After: Split by Responsibility
// UserAuth.js
export function login() { ... }
export function register() { ... }

// UserHelpers.js
export function validatePassword() { ... }
export function formatAddress() { ... }

// UserImages.js
export function updateAvatar() { ... }

The "Shared" or "Common" Folder

You will inevitably have code that is used by multiple features (e.g., a Date Formatter or a Database Client).

Create a folder named `shared`, `common`, or `core`. Ideally, keep this flat.

/src
  /features
    /users
    /orders
  /shared
    /ui
      Button.js
      Modal.js
    /utils
      date.js
      currency.js
    apiClient.js

The Golden Rule of Dependencies:
Features can import from `shared`.
`shared` can never import from `features`.
Features should ideally not import from other features (this causes tight coupling). If Feature A needs logic from Feature B, consider moving that logic to `shared` or using an Event Bus.

Circular Dependencies

One of the biggest risks of poor file structure is the Circular Dependency: File A imports File B, and File B imports File A.

This usually happens when you split code based on "Category" rather than "Hierarchy."

Example:
`User.js` needs `Order.js` (to get a user's order history).
`Order.js` needs `User.js` (to get the order's owner name).

Fix: Create a third file that sits "below" both of them, or pass the data as arguments. Alternatively, use dependency injection where `User` doesn't import `Order` class directly but receives an `orderService` instance.

The Barrel Pattern (index.js)

To keep your imports clean, use an `index.js` file in your folders. This is known as the Barrel Pattern.

// src/features/users/index.js
export { default as UserProfile } from './UserProfile';
export { loginUser } from './auth';
export { PREFERENCES } from './constants';

// Now, other files import like this:
import { UserProfile, loginUser } from '../features/users';

This acts as a "Public API" for your folder. It allows you to reorganize the files inside the `users` folder without breaking the rest of the application that imports from it.

Summary

Don't let architectural decisions stop you from starting. The best architecture is the one that is easy to change later.

The Pragmatic Checklist:

← Back to Architecture & Workflow

Back to Home