Skip to main content

Command Palette

Search for a command to run...

Folder Structure Patterns — Feature-Based vs Type-Based, and Why It Matters

Day 16 of 40 — React System Design Series

Updated
8 min read
Folder Structure Patterns — Feature-Based vs Type-Based, and Why It Matters
R

Hi, I’m Richa — a Senior Frontend Engineer with 5+ years of experience building scalable, production-grade web interfaces for enterprise and consumer applications. I work primarily with React, TypeScript, and modern frontend architectures, focusing on component systems, performance, and maintainability. Most of my experience comes from building real-world products in regulated domains like banking and insurance, where clarity, reliability, and long-term ownership matter more than quick demos. Through this blog, I write about frontend engineering fundamentals, scalable UI design, problem-solving, and the lessons I’ve learned working on large codebases. My goal is to share practical insights — not shortcuts — for developers who want to grow strong engineering foundations. I also mentor early-career developers and strongly believe that curiosity, asking the right questions, and understanding why something works are more important than memorizing tools. If you’re serious about improving as an engineer, you’re in the right place.

Hey everyone, Richa here! 👋

organize gif

Day 16. Week 4. And we're talking about something that sounds boring but causes enormous pain in real teams.

Folder structure.

I've worked on codebases where finding a file took longer than writing the code. Where you'd open components/ and see 87 files in a flat list. Where two developers working on different features were constantly touching the same folders and creating merge conflicts.

And I've worked on codebases where the structure was so clear that a new team member could find anything in under a minute on their first day.

The difference wasn't luck. It was a deliberate structure decision made early — or not made at all.

Today we make it properly.


The two main approaches

Almost every React project uses one of two folder structure philosophies. Or an accidental mix of both.

Type-based structure — organise by what kind of file it is:

src/
  components/
  hooks/
  pages/
  services/
  utils/
  types/
  context/

Feature-based structure — organise by what part of the app it belongs to:

src/
  features/
    auth/
    dashboard/
    products/
    cart/
  shared/
    components/
    hooks/
    utils/

Both are valid choices. Both have failure modes. The right choice depends on the size and growth trajectory of your app.


Type-based structure — when it works and when it breaks

Type-based is the default. Every tutorial, every create-react-app project, most "getting started" docs use it.

src/
  components/
    Button.tsx
    Input.tsx
    Modal.tsx
    UserCard.tsx
    ProductList.tsx
    CartItem.tsx
    OrderSummary.tsx
    CheckoutForm.tsx
    AdminUserTable.tsx
    ... (87 more)
  hooks/
    useAuth.ts
    useCart.ts
    useProducts.ts
    useOrders.ts
    ...
  pages/
    LoginPage.tsx
    DashboardPage.tsx
    ProductsPage.tsx
    CartPage.tsx
    ...
  services/
    authService.ts
    productService.ts
    cartService.ts
    ...

When it works:

  • Small apps (< 10 features)
  • Solo projects or very small teams
  • Projects that won't grow much

When it breaks:

You're working on a new Products feature. You need to touch:

  • components/ProductList.tsx — add a filter bar
  • components/ProductCard.tsx — add a wishlist button
  • hooks/useProducts.ts — add filtering logic
  • services/productService.ts — add filter API call
  • pages/ProductsPage.tsx — wire it together
  • types/product.ts — add filter types

Six files. Five different folders. A code review for one feature spans your entire src/ directory. Delete the feature? You have to hunt through every folder to find all the related files.

This is the problem type-based structure creates as apps grow.


Feature-based structure — the scalable approach

Feature-based puts everything related to a feature in one place.

src/
  features/
    auth/
      components/
        LoginForm.tsx
        LogoutButton.tsx
      context/
        AuthContext.tsx
      hooks/
        useAuth.ts
      services/
        authService.ts
      types/
        auth.types.ts
      index.ts          ← public API for this feature
    products/
      components/
        ProductCard.tsx
        ProductList.tsx
        ProductFilters.tsx
      hooks/
        useProducts.ts
        useProductFilters.ts
      services/
        productService.ts
      types/
        product.types.ts
      index.ts
    cart/
      components/
        CartItem.tsx
        CartSidebar.tsx
        CartIcon.tsx
      hooks/
        useCart.ts
      stores/
        cartStore.ts      ← Zustand store
      types/
        cart.types.ts
      index.ts
  shared/
    components/
      Button.tsx
      Input.tsx
      Modal.tsx
      Spinner.tsx
      ErrorMessage.tsx
    hooks/
      useDebounce.ts
      useLocalStorage.ts
      useMediaQuery.ts
    utils/
      formatDate.ts
      formatCurrency.ts
      cn.ts              ← class name utility
    types/
      common.types.ts
  pages/
    LoginPage.tsx
    DashboardPage.tsx
    ProductsPage.tsx
    CartPage.tsx
  app/
    App.tsx
    routes.tsx
    store.ts            ← RTK configureStore if using Redux

Now working on the Products feature:

  • Everything is in features/products/
  • Delete the feature? Delete the folder.
  • Code review? Reviewer looks in one place.
  • New team member? "The products feature is in features/products/" — done.

The index.ts barrel file — controlling your public API

Each feature has an index.ts that defines what the rest of the app can import:

// features/auth/index.ts
export { AuthProvider, useAuthContext } from './context/AuthContext';
export { LoginForm } from './components/LoginForm';
export { LogoutButton } from './components/LogoutButton';
export type { User, LoginCredentials } from './types/auth.types';

// Don't export internal implementation details:
// authService — internal to the feature
// useAuth — internal hook used by AuthProvider

Now imports across the app look like:

// ✅ Clean — importing from the feature's public API
import { useAuthContext, LoginForm } from '@/features/auth';
import { useProducts } from '@/features/products';
import { Button, Spinner } from '@/shared/components';

// ❌ Messy — deep imports that expose internals
import { useAuthContext } from '../../features/auth/context/AuthContext';
import { LoginForm } from '../../../features/auth/components/LoginForm';

The barrel file is a contract. It says "here's what this feature exposes to the outside world." Internals can be reorganised freely without breaking anything outside the feature.


Path aliases — eliminating ../../.. hell

Set up path aliases in tsconfig.json and your bundler config. This alone makes imports dramatically cleaner.

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@features/*": ["src/features/*"],
      "@shared/*": ["src/shared/*"],
      "@pages/*": ["src/pages/*"]
    }
  }
}
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@features': path.resolve(__dirname, './src/features'),
      '@shared': path.resolve(__dirname, './src/shared'),
      '@pages': path.resolve(__dirname, './src/pages'),
    },
  },
});

Before:

import { useAuthContext } from '../../../features/auth/context/AuthContext';
import { Button } from '../../shared/components/Button';
import { formatDate } from '../../../shared/utils/formatDate';

After:

import { useAuthContext } from '@features/auth';
import { Button } from '@shared/components';
import { formatDate } from '@shared/utils/formatDate';

Much cleaner. And if you move a file — you fix the path alias config once, not every file that imports it.


The shared folder — what goes there

shared/ is for code that is used by two or more features and doesn't belong to any single one.

shared/
  components/   ← generic UI: Button, Input, Modal, Spinner, Badge, Avatar
  hooks/        ← generic hooks: useDebounce, useLocalStorage, useMediaQuery, usePrevious
  utils/        ← pure functions: formatDate, formatCurrency, cn(), validateEmail
  types/        ← shared TypeScript types: Pagination, ApiResponse, SortOrder
  constants/    ← app-wide constants: API_BASE_URL, DATE_FORMATS, ROUTES

The rule for shared/: if a component or utility is used by only one feature — it lives inside that feature. Only when a second feature needs it does it move to shared/.

// ✅ Button used by auth, products, cart, admin → shared/components/Button.tsx
// ✅ useDebounce used by search and filters → shared/hooks/useDebounce.ts
// ❌ ProductFilters only used by products → features/products/components/ProductFilters.tsx

Don't prematurely put things in shared/. Start inside the feature. Move to shared/ when a second consumer appears.


Pages — the thin orchestration layer

Pages are thin. They import from features and wire things together. No business logic. No direct API calls.

// pages/ProductsPage.tsx
import { ProductList, ProductFilters, useProducts } from '@features/products';
import { useAuthContext } from '@features/auth';
import { Spinner, ErrorMessage } from '@shared/components';

export function ProductsPage() {
  const { user } = useAuthContext();
  const { products, isLoading, isError } = useProducts();

  if (isLoading) return <Spinner />;
  if (isError) return <ErrorMessage message="Failed to load products" />;

  return (
    <div className="products-page">
      <h1>Products</h1>
      <ProductFilters />
      <ProductList products={products} />
    </div>
  );
}

Pages are the glue layer. They compose feature components — they don't contain logic themselves.


A realistic mid-size app structure — the full picture

src/
  app/
    App.tsx
    routes.tsx
  features/
    auth/
      components/   LoginForm, LogoutButton, ForgotPasswordForm
      context/      AuthContext
      hooks/        useAuth (internal)
      services/     authService, apiClient
      types/        auth.types.ts
      index.ts
    products/
      components/   ProductCard, ProductList, ProductFilters, ProductDetail
      hooks/        useProducts, useProduct, useProductFilters
      services/     productService
      stores/       productFiltersStore (Zustand)
      types/        product.types.ts
      index.ts
    cart/
      components/   CartItem, CartSidebar, CartIcon, CartEmptyState
      hooks/        useCart (internal)
      stores/       cartStore (Zustand)
      types/        cart.types.ts
      index.ts
    orders/
      components/   OrderList, OrderCard, OrderDetail
      hooks/        useOrders, useOrder
      services/     orderService
      types/        order.types.ts
      index.ts
    admin/
      components/   AdminUserTable, AdminStats, AdminSidebar
      hooks/        useAdminUsers
      services/     adminService
      types/        admin.types.ts
      index.ts
  shared/
    components/   Button, Input, Modal, Spinner, Badge, ErrorMessage, EmptyState
    hooks/        useDebounce, useLocalStorage, useMediaQuery, usePrevious
    utils/        formatDate, formatCurrency, cn, validateEmail
    types/        common.types.ts (Pagination, ApiResponse, etc.)
    constants/    routes.ts, api.ts
  pages/
    LoginPage, DashboardPage, ProductsPage, CartPage, OrdersPage, AdminPage

Every new developer on this team can answer:

  • "Where is the cart logic?"features/cart/
  • "Where is the shared Button component?"shared/components/Button.tsx
  • "Where is the Products page wired together?"pages/ProductsPage.tsx

In under 10 seconds. Every time.


Which structure for which app?

App size Team size Recommendation
< 5 features 1–2 devs Type-based is fine — don't over-engineer
5–15 features 2–5 devs Start feature-based now — easier to do it early
15+ features 5+ devs Feature-based is not optional — type-based collapses
Micro-frontend Large team Feature-based + consider separate packages per domain

Today's takeaway

Type-based structure (components/, hooks/, pages/) is fine for small apps. It stops scaling around 10–15 features.

Feature-based structure (features/auth/, features/products/) scales indefinitely. Each feature is self-contained — find it, change it, delete it without touching anything else.

The migration from type-based to feature-based is painful. The best time to do it is before you feel the pain — when the app is still small enough that it takes an afternoon.

The second best time is now.

organized gif

See you tomorrow for Day 17 — Advanced component patterns: compound components, render props, and HOCs.


Day 15 — Refresh Tokens and Silent Refresh

Day 17 — Advanced Component Patterns


Part of the React System Design Series — 40 days, one topic per day.

React System Design — Learning in Public

Part 16 of 34

I'm a React developer with 5.5 years of experience who spent 2 years in Lit.js — and slowly forgot how React really works at a senior level. This series is my public journey back. Every day I cover one topic: state management, authentication, performance, system design interviews, testing — the real stuff that separates a mid-level dev from a senior. Written honestly, with real code, for developers at every level.

Up next

Advanced Component Patterns — Compound Components, Render Props, and HOCs

Day 17 of 40 — React System Design Series