Topic #48Core5 min read

Project Structure (Best Practice)

Feature-based organization scales better than type-based folders for non-trivial apps.

#project-structure#architecture#feature-based#organization

As an app grows, the default "type-based" structure (all components in components/, all hooks in hooks/, all slices in store/) becomes painful: a single feature is scattered across the whole tree, and unrelated features sit next to each other. Feature-based organization flips this: each feature owns a folder containing its own components/, hooks/, api/, store/, and types/, with an index.ts that defines the feature's public surface.

Truly cross-cutting code lives under shared/ (generic Button, Modal, Spinner, utility hooks like useToggle/useDebounce, helpers like formatCurrency). App-wide concerns get their own top-level folders: config/ for env, constants and routes; styles/ for global CSS and the Tailwind config. This keeps the boundary between "this feature" and "everyone" explicit.

The big win is colocation and encapsulation: when you work on expenses, everything you need is in features/expenses/, and the index.ts barrel controls what the rest of the app is allowed to import. This reduces accidental coupling, makes features easy to move or delete, and keeps imports shallow and predictable.

Backend Analogy

Feature-based folders are the frontend version of organizing a Spring service by domain package (com.dice.expense.*) rather than by layer (all controllers in one package, all repositories in another). The feature's index.ts is like a package-private boundary or a module-info — it declares the public API and hides the internals.

Key Insights
  • Feature-based structure colocates components, hooks, api, store, and types per feature — type-based scatters one feature across the whole tree.
  • Each feature's index.ts is its public contract; importing internals directly across features is a code smell.
  • shared/ holds only genuinely generic, reusable building blocks (Button, Modal, useDebounce); config/ holds env, constants, and routes.

Worked Code

Feature-based structure (recommended for Dice-scale apps)
Shell
// Feature-based structure (recommended for Dice-scale apps)
src/
├── features/
│   ├── expenses/
│   │   ├── components/    // ExpenseCard, ExpenseList, ExpenseForm
│   │   ├── hooks/         // useExpenses, useExpenseForm
│   │   ├── api/           // expense API calls
│   │   ├── store/         // expenseSlice (Redux)
│   │   ├── types/         // Expense interface
│   │   └── index.ts       // public exports
│   ├── auth/
│   │   ├── components/
│   │   ├── hooks/
│   │   └── context/       // AuthContext
│   └── dashboard/
├── shared/
│   ├── components/        // Button, Modal, Spinner
│   ├── hooks/             // useToggle, useDebounce
│   ├── utils/             // formatCurrency, dateHelpers
│   └── types/             // global types
├── config/                // env, constants, routes
├── styles/                // global CSS, Tailwind config
├── App.tsx
└── main.tsx

Interview-Ready Q&A

Type-based groups files by their kind (components/, hooks/, store/) and works fine for small apps. Feature-based groups by domain (features/expenses/ owning its own components, hooks, api, store, types). You switch to feature-based once a feature spans many files and multiple types — it colocates everything for that feature, limits accidental coupling, and makes the feature easy to move or delete.

Things to Remember
  • 1Feature-based: each feature owns its components/hooks/api/store/types plus an index.ts public barrel.
  • 2shared/ = generic reusable pieces only; config/ = env, constants, routes; styles/ = global CSS + Tailwind config.
  • 3Avoid deep cross-feature imports — go through the feature's index.ts.

References & Further Reading