Folder Structure Patterns — Feature-Based vs Type-Based, and Why It Matters
Day 16 of 40 — React System Design Series

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! 👋

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 barcomponents/ProductCard.tsx— add a wishlist buttonhooks/useProducts.ts— add filtering logicservices/productService.ts— add filter API callpages/ProductsPage.tsx— wire it togethertypes/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.

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.






