How to Split Code into Files and Folders Without Overthinking It
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:
- Focus: When you work on Orders, you only open the `orders` folder. Everything you need is right there.
- Deletability: If you delete the "Products" feature, you just delete the `products` folder. In the Layer-based approach, you have to hunt down files in five different directories.
- Scalability: As the app grows, you add more feature folders. You don't end up with a `controllers` folder containing 500 files.
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:
- Start Flat: Don't create folders until you have enough files to fill them.
- Group by Feature: Keep related things together (UI, Logic, Styles).
- Extract Shared Logic: Move truly reusable code to `shared/`.
- Refactor Later: Move files around when they feel painful, not before. Modern IDEs make moving files safe and easy.