Available for freelanceContact me so I can help your business grow or turn your idea into reality!

I'm interested
Frontend Folder Structure That Scales

Frontend Folder Structure That Scales

Every project starts clean. Six months later, components/ has 80 files, nobody knows where anything lives, and onboarding takes a week.

The folder structure isn't the problem. The absence of rules is.


The Two Common Approaches

Type-based (common early on)

components/
  Button.tsx
  Modal.tsx
  ProductCard.tsx
  UserProfile.tsx
hooks/
  useCart.ts
  useAuth.ts
  useProducts.ts
utils/
  formatDate.ts
  formatPrice.ts

This looks organized. But as the app grows, components/ becomes a dumping ground. Nothing is grouped by what it belongs to.


Feature-based (what actually scales)

features/
  cart/
    components/
    hooks/
    utils/
    index.ts
  auth/
    components/
    hooks/
    utils/
    index.ts
  products/
    components/
    hooks/
    utils/
    index.ts

Each feature owns its code. You can read, change, or delete a feature without hunting across the repo.


A Practical Structure for Next.js App Router

app/
  [locale]/
    (marketing)/
      page.tsx
    (app)/
      dashboard/
        page.tsx
      settings/
        page.tsx
    layout.tsx

components/
  ui/           # generic, reusable (Button, Input, Modal)
  layout/       # structural (Header, Footer, Sidebar)

features/
  auth/
    components/
      LoginForm.tsx
      AuthGuard.tsx
    hooks/
      useAuth.ts
    actions/
      login.ts
    index.ts
  cart/
    components/
      CartDrawer.tsx
      CartItem.tsx
    hooks/
      useCart.ts
    store/
      cart.store.ts
    index.ts

lib/
  db.ts
  flags.ts
  analytics.ts

hooks/           # truly global hooks only
utils/           # truly global utilities only
types/           # shared TypeScript types

The Colocation Principle

Keep code close to where it's used.

If a hook is only used inside CartDrawer.tsx, it doesn't belong in /hooks. It belongs next to the component.

features/cart/
  components/
    CartDrawer.tsx
    CartDrawer.hooks.ts    # only used here
  hooks/
    useCart.ts             # used across the cart feature

Move up the tree only when something is shared across features. Don't move things "just in case".


The index.ts Barrel Pattern

Use index.ts to define what a feature exposes publicly.

// features/cart/index.ts
export { CartDrawer } from './components/CartDrawer'
export { useCart } from './hooks/useCart'
export type { CartItem } from './types'

Then import cleanly from other features:

import { CartDrawer, useCart } from '@/features/cart'

This is your feature's public API. Internal files stay internal.


When Barrel Files Go Wrong

Barrel files improve imports — but they have a cost.

// ❌ Every file in the app gets bundled when this is imported
export * from './Button'
export * from './Modal'
export * from './ProductCard'
// ...50 more exports

Tree-shaking breaks down with large barrel files — and your bundle pays for it.

Rule: Use barrel files at the feature level (not for components/ui/). Keep UI barrels small and explicit.


Route Groups in Next.js App Router

Use route groups (group-name) to separate concerns without affecting the URL.

app/
  [locale]/
    (marketing)/
      page.tsx          → /en
      about/
        page.tsx        → /en/about
    (app)/
      layout.tsx        → shared authenticated layout
      dashboard/
        page.tsx        → /en/dashboard
      settings/
        page.tsx        → /en/settings

Marketing pages and app pages share the locale param but have different layouts. Route groups keep them separated cleanly.


Common Mistakes

1. One giant components/ folder

components/
  LoginButton.tsx
  ProductCard.tsx
  CartItem.tsx
  DashboardHeader.tsx
  AdminTable.tsx
  ... 70 more files

No grouping = no structure. Extract to features.


2. Global hooks/ for feature-specific hooks

// ❌ hooks/useCartDiscount.ts — only used in cart
// ✅ features/cart/hooks/useCartDiscount.ts

3. Deep nesting

features/
  checkout/
    components/
      steps/
        payment/
          forms/
            fields/
              CreditCardField.tsx

Past 4 levels, navigation becomes painful. Flatten when nesting adds no meaning.


4. Importing across feature boundaries directly

// ❌ Couples features together directly
import { CartItem } from '../cart/components/CartItem'
// ✅ Import through the public API
import { CartItem } from '@/features/cart'

This makes refactoring a single feature safe without breaking others.


When to Refactor Your Structure

SignalAction
"Where does this file go?" — everyone asksClarify the rules, write them down
Feature files are spread across 3+ foldersGroup by feature
Deleting a feature requires searching the whole repoReorganize by feature
New devs take >1 hour to find thingsToo much nesting or no conventions

What to Do Next

  • Audit your current structure: count files per folder, spot the dumping grounds
  • Pick one feature and move it into features/your-feature/
  • Add an index.ts to define its public API
  • Write down the rules — even a 5-line README in /features helps onboarding

Structure is not about perfection. It's about making the next decision obvious.

When a new file lands, the right folder should be clear without a team discussion. That's the goal.