Six months ago I joined a project with 200+ components in a single components/ folder. No subfolders. Just a flat list from AccordionItem.jsx to ZoomControls.jsx. Finding anything meant Cmd+P and hoping you guessed the filename right.

The codebase worked. Tests passed. But adding features was painfully slow because nobody could find anything. We spent a full sprint just reorganizing files.

Folder structure isn't sexy, but it determines how fast your team can move. Here's what I've learned about getting it right.

The Kitchen Drawer Problem

Imagine organizing a kitchen two ways:

Option A: One drawer for all utensils. Forks, knives, whisks, spatulas, measuring cups—everything together. Easy to put things away. Nightmare to find what you need.

Option B: Drawers by activity. "Baking" has measuring cups and whisks. "Eating" has forks and knives. "Prep" has cutting boards and peelers. You know exactly where to look.

Most codebases start with Option A:

src/
  components/   ← All 200 components here
  hooks/        ← All hooks here  
  utils/        ← All utilities here
  services/     ← All API calls here

It's organized by type. But when you're fixing the checkout flow, you need to open files from four different folders. Related code is scattered everywhere.

Feature-Based Organization

The alternative is organizing by feature:

src/
  features/
    checkout/
      CheckoutForm.jsx
      CheckoutForm.test.js
      useCheckout.js
      checkoutApi.js
      formatPrice.js
    auth/
      LoginForm.jsx
      useAuth.js
      authApi.js
    dashboard/
      ...
  shared/
    components/
      Button.jsx
      Modal.jsx
    hooks/
      useDebounce.js

Now everything related to checkout lives together. When you're working on that feature, you're mostly in one folder. The mental overhead drops dramatically.

The Colocation Principle

Here's a rule I follow: keep things as close to where they're used as possible.

If a hook is only used by one component, put it in the same folder as that component. If a utility function is only used in the checkout feature, put it in the checkout folder.

Only move things to shared/ when they're actually shared by multiple features.

features/
  checkout/
    CheckoutForm.jsx
    CheckoutForm.css        ← Styles right here
    CheckoutForm.test.js    ← Tests right here
    useCheckoutValidation.js ← Hook only used here

Some people hate this—they want all CSS in one place, all tests in one place. But I've found that "all X in one place" makes individual files easy to find and features hard to understand.

The Barrel Pattern

One downside of deep folder structures is ugly imports:

import { CheckoutForm } from '../../../features/checkout/components/CheckoutForm';
import { useCheckout } from '../../../features/checkout/hooks/useCheckout';

The barrel pattern cleans this up. Add an index.js to each feature that exports the public API:

// features/checkout/index.js
export { CheckoutForm } from './CheckoutForm';
export { useCheckout } from './useCheckout';
export { CheckoutProvider } from './CheckoutProvider';

Now imports are cleaner:

import { CheckoutForm, useCheckout } from '@/features/checkout';

Bonus: this creates a clear boundary. Anything not exported from index.js is an internal implementation detail. Other features shouldn't depend on it.

When to Extract to Shared

I follow the "rule of three": don't move something to shared/ until it's used in three different places.

Premature sharing creates coupling. You extract a "generic" Button, then realize each feature needs slightly different behavior. Now you're adding props and flags to handle every case, and your "simple" shared component becomes a mess.

It's easier to merge three similar implementations into one generic one than to untangle a premature abstraction.

Separating UI from Logic

One more thing: keep your components dumb. They should receive data and render it. Business logic goes in hooks or services.

// ❌ Mixed concerns
function CheckoutForm() {
    const [items, setItems] = useState([]);
    const [total, setTotal] = useState(0);
    
    useEffect(() => {
        fetch('/api/cart').then(r => r.json()).then(data => {
            setItems(data.items);
            setTotal(data.items.reduce((sum, i) => sum + i.price, 0));
        });
    }, []);
    
    const handleSubmit = async () => {
        await fetch('/api/checkout', { method: 'POST', body: JSON.stringify(items) });
        // ... more logic
    };
    
    return 
...
; } // ✅ Separated function CheckoutForm() { const { items, total, submitOrder, isLoading } = useCheckout(); return (
{/* Pure rendering, no logic */}
); }

The second version is easier to test, easier to understand, and the hook can be reused if you need checkout logic elsewhere.

Practical Advice

If you're starting a new project: start with feature folders from day one. It's much harder to reorganize later.

If you're inheriting a messy codebase: don't try to reorganize everything at once. Move things gradually as you work on them. After a few months, the hot paths will be organized and the rarely-touched code... doesn't matter as much anyway.

The goal isn't a perfect folder structure. It's a structure that lets your team find things quickly and understand how pieces connect. Everything else is details.

← Back to JavaScript Articles

Back to Home