We’ve all been there. You clone a new project, pop open the src folder, and your heart just… sinks. It’s a digital junk drawer. A tangled mess of components, utils, helpers, services, and a dozen other vaguely named directories. Finding where to make a simple change feels like a treasure hunt, but you don't even have a map.
Let’s be real—when you first start a new React project, folder structure is probably the last thing on your mind. You're riding that initial wave of creative energy, totally focused on building cool stuff, and create-react-app gives you a perfectly fine place to start. So you throw a few components in a components folder, some API calls in a services folder, and you’re off to the races.
And for a little while, it works beautifully.
But then, the project grows. And grows. What was once a tidy little app becomes a sprawling codebase. Suddenly, onboarding a new developer takes a week instead of an afternoon. A tiny bug fix requires you to touch files in five different directories. That initial speed? It's gone, replaced by a slow, creeping dread. This is the inevitable outcome of a structure that simply doesn't scale.
If any of this is hitting a little too close to home, you're in the right place. I’ve spent years navigating these exact problems, both on my own side projects and on massive, enterprise-level applications. Today, I’m going to walk you through a battle-tested approach for structuring large React apps that prioritizes clarity, scalability, and—most importantly—our sanity as developers.
This isn’t about rigid, dogmatic rules. Think of it as a flexible blueprint that helps you build software that's a joy to work on, even when it gets complicated.
The Great Debate: Folder-by-Type vs. Feature-Based
Before we dive into the blueprint, we need to address a fundamental choice every React developer faces. It really boils down to two main philosophies.
1. The "Folder-by-Type" Approach (The Junk Drawer)
This is the one we all seem to start with. It feels intuitive, right? You group files by what they are.
src/
├── api/
│ ├── userAPI.js
│ └── productAPI.js
├── components/
│ ├── Button.js
│ ├── UserProfile.js
│ ├── ProductCard.js
│ └── Navbar.js
├── hooks/
│ ├── useUser.js
│ └── useProducts.js
└── pages/
├── HomePage.js
└── ProfilePage.js
Can you already feel the problem? To work on the "User Profile" feature, you have to jump between components/UserProfile.js, hooks/useUser.js, and api/userAPI.js. These files are functionally joined at the hip, but they live miles apart in the file tree. As the app grows, these folders become massive, unwieldy lists. The cognitive load is huge because the project's structure doesn't actually reflect the project's features.
It's like organizing a kitchen by putting all the metal things in one drawer, all the plastic things in another, and all the wooden things in a third. I mean, sure, it's a system, but good luck making a sandwich.
2. The "Feature-Based" Approach (The Organized Workshop)
Now, let's imagine a different way. What if we organized our code by what it does? We group all the files related to a single feature into one self-contained module.
src/
└── features/
├── products/
│ ├── components/
│ │ └── ProductCard.js
│ ├── hooks/
│ │ └── useProducts.js
│ └── api/
│ └── productAPI.js
└── profile/
├── components/
│ └── UserProfile.js
├── hooks/
│ └── useUser.js
└── api/
└── userAPI.js
Suddenly, everything just clicks. Want to work on the user profile? Everything you could possibly need is right there in src/features/profile. The code is colocated. It’s easy to find, it's easy to understand, and—here's the killer part—it's easy to delete. If you decide to remove the profile feature, you just delete one folder. Done.
For any application that’s more than a handful of pages, I truly believe the feature-based approach is non-negotiable. It’s the foundation of a scalable and maintainable React architecture.
My Go-To Blueprint: A Scalable React Architecture
Okay, theory is great, but let's get practical. Here is the exact folder structure I use as a starting point for any serious React application. It’s a hybrid model that fully embraces the feature-based approach while still providing logical homes for those truly global concerns.
src/
├── assets/ # Static files like images, fonts
├── components/ # Truly shared, reusable, "dumb" components
├── config/ # Environment variables, constants
├── features/ # The heart of the app, feature modules live here
├── hooks/ # Global, reusable hooks (e.g., useLocalStorage)
├── lib/ # External library configurations (e.g., axios instance)
├── providers/ # All React Context providers
├── routes/ # App routing configuration
├── stores/ # Global state management stores (Zustand, Redux)
├── styles/ # Global styles, theme definition
├── types/ # Global TypeScript types
└── utils/ # Truly shared, generic utility functions
Let's break this down, folder by folder.
src/components - The UI Toolkit
This is probably one of the most misunderstood folders in many React projects. This is NOT a dumping ground for every component in your app. This folder is reserved for your application's "design system" or UI toolkit.
Think of components that are completely generic and have zero business logic tied to any specific feature.
Button.tsxInput.tsxModal.tsxSpinner.tsxCard.tsx
These are your fundamental building blocks. A Button in this folder doesn't know if it's submitting a user profile or adding a product to a cart. It just knows how to be a button. This separation is absolutely crucial for reusability.
src/features - Where the Magic Happens
Okay, this is the core of our architecture. Each sub-directory inside features represents a distinct slice of your application's functionality. The goal is for each feature to be as self-contained as possible.
Let's imagine we're building a simple e-commerce dashboard. We might have features like:
features/productsfeatures/ordersfeatures/authfeatures/user-profile
Now, let's peek inside a single feature folder, say user-profile.
src/
└── features/
└── user-profile/
├── api/
│ ├── index.ts # API client functions (e.g., getUser, updateUser)
│ └── mutations.ts # (Optional) React Query mutations
├── components/
│ ├── ProfileHeader.tsx
│ ├── EditProfileForm.tsx
│ └── AvatarUpload.tsx
├── hooks/
│ └── useUserProfile.ts # Hook to fetch and manage profile data
├── types/
│ └── index.ts # TypeScript types specific to this feature
├── utils/
│ └── formatters.ts # Utility functions only used here
├── index.ts # The "public" API of this feature
└── UserProfilePage.tsx # The main page component for this feature
Look at that. Isn't it beautiful? Every single file related to the "user profile" is neatly organized within its own little world.
Now, the most important file here is index.ts. Think of this as the public entry point for the feature. It should only export what other parts of the app absolutely need to know about. This practice enforces clear boundaries and prevents other features from reaching into the internal workings of this one.
Here’s what that index.ts might look like:
// src/features/user-profile/index.ts
// Export the main page component
export { UserProfilePage } from './UserProfilePage';
// Export any public hooks or components if needed elsewhere
export { useUserProfile } from './hooks/useUserProfile';
Now, over in our router, we can simply import UserProfilePage from @/features/user-profile without ever worrying about its internal file paths. This is encapsulation, but at the file-system level. It's a game-changer.
src/lib, src/utils, src/hooks - The Global Toolbelt
These folders are for code that is genuinely shared across the entire application and doesn't really belong to any single feature.
lib/: This is my go-to place for configuring and exporting instances of third-party libraries. A classic example is setting up an Axios instance with base URLs and interceptors. You configure it once here and then import that instance everywhere you need it.utils/: For pure, generic helper functions. ThinkformatDate,cn(for class names), ordebounce. But be strict about this! If a utility is only used inside one feature, it belongs inside that feature'sutilsfolder. I've seenutils.jsfiles that are thousands of lines long—that's a major code smell.hooks/: This is for hooks that aren't tied to a specific feature's data. Good examples includeuseLocalStorage,useMediaQuery, oruseDebounce. A hook likeuseUserProfile, which is all about user profile data, belongs squarely in theuser-profilefeature.
src/routes - The Application Map
This is where you define your application's navigation. I'm a huge fan of keeping all route definitions in one place because it provides a wonderful, high-level overview of the entire app's structure.
Using a library like react-router-dom, you might have a central index.tsx file that looks something like this:
// src/routes/index.tsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { AppLayout } from '@/components/Layout'; // A shared layout component
import { HomePage } from '@/pages/HomePage'; // A simple, non-feature page
import { UserProfilePage } from '@/features/user-profile';
import { ProductListPage } from '@/features/products';
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
children: [
{
index: true,
element: <HomePage />,
},
{
path: 'profile',
element: <UserProfilePage />,
},
{
path: 'products',
element: <ProductListPage />,
},
// ... more routes
],
},
]);
export const AppRouter = () => <RouterProvider router={router} />;
See how clean those imports are? We're pulling in the top-level page components directly from our features. The whole file reads like a table of contents for the application.
Visualizing the Flow: Your Architecture Diagram
Talking about structure is one thing, but seeing it is another. For large apps, an architecture diagram isn't just a nice-to-have; it's an essential communication tool. It helps everyone—from new hires to senior architects—get on the same page about how data and control flow through the system.
And you don't need fancy software for this. A simple tool like Excalidraw or even Mermaid syntax in your Markdown is perfect.
Here’s how you might sketch out a simple data flow for our user profile feature:
- Start with the User Action: A box labeled "User navigates to
/profile". - The Entry Point: An arrow points to "React Router".
- Routing: React Router matches the path and renders the
UserProfilePagecomponent. Draw a box forUserProfilePage. - Data Fetching: Inside
UserProfilePage, theuseUserProfilehook is called. Draw an arrow from the page to a new box foruseUserProfile. - API Layer: The hook then calls a function like
getUser()fromfeatures/user-profile/api. An arrow points from the hook to anAPI Layerbox. - External Service: The API layer makes an HTTP request to your backend. Draw an arrow from the API layer to a cylinder representing your "Backend Server / Database".
- The Return Trip: Finally, draw arrows flowing back the other way, showing how the data is passed from the server, through the API layer, into the hook's state, and finally rendered by the components on the
UserProfilePage.
Here's a simplified text version of that flow:
(User) --> [Browser URL: /profile]
|
v
[React Router] -- Renders --> [UserProfilePage.tsx]
|
v
[useUserProfile.ts Hook] -- Calls --> [api/index.ts]
|
v
[Axios Instance (from lib)] -- HTTP GET --> (Backend API)
|
v
(JSON Data) -- Returns --> [api/index.ts]
|
v
[useUserProfile.ts Hook] (updates state)
|
v
[UserProfilePage.tsx] -- Re-renders with data --> (UI is displayed to User)
This kind of diagram is honestly invaluable. It makes abstract concepts concrete and provides a shared understanding for the entire team. Do yourself a favor and put it in your project's README.md!
Beyond Folders: Other Pillars of a Strong Architecture
A great structure isn't just about directories. Here are a few other things that contribute to a clean, scalable app.
State Management: Please, don't reach for a global state manager like Redux or Zustand by default. Start with React's built-in hooks (useState, useReducer, useContext). A fantastic library like React Query (TanStack Query) can handle almost all of your server state, caching, and background refetching for you.
Here's my personal rule of thumb:
- Is it server state? Use React Query. Place your queries and mutations within their respective feature folders (
features/feature-name/api/). - Is it global UI state (e.g., theme, open/closed mobile nav)? Use Zustand or a simple React Context. Keep these in
src/storesorsrc/providers. - Is it local component state? Just use
useState. Keep it simple!
Styling: The key here is just consistency. Pick one strategy and stick with it.
- Tailwind CSS: My personal favorite for its utility-first approach. It colocates styles directly in your JSX, which fits perfectly with a component-based architecture.
- CSS Modules: Great for scoped CSS without the overhead of a CSS-in-JS library.
- Styled-Components / Emotion: Powerful CSS-in-JS solutions that allow for dynamic, prop-based styling.
Whichever you choose, define your theme (colors, spacing, fonts) in a global location like src/styles/theme.ts so everyone is using the same design tokens.
Absolute Imports: Nothing screams "messy project" quite like import Button from '../../../../components/Button'. Take a few minutes to configure absolute imports in your tsconfig.json or jsconfig.json.
// tsconfig.json
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["*"]
}
}
}
Now your imports become beautiful and, more importantly, maintainable: import Button from '@/components/Button'. It doesn't matter how deeply nested your file is.
Finalizing the Vision: It's About People
We've covered a lot of ground here—from high-level philosophy to nitty-gritty folder names. But at the end of the day, a good architecture serves one primary purpose: it makes it easier for humans to build and maintain software.
It’s not about finding the one, perfect, universal folder structure that will solve all our problems. It’s about agreeing on a set of conventions that reduces cognitive load, improves communication, and empowers your team to move faster and with more confidence.
The blueprint I've laid out is a starting point. Adapt it. Challenge it. Make it your own. The best structure is the one your team understands and uses consistently. Start with this foundation, and you'll be well on your way from a chaotic codebase to a clear, scalable, and genuinely enjoyable React application.
Frequently Asked Questions
When should I switch from a simple "folder-by-type" to this "feature-based" structure?
Honestly? I'd say almost immediately. The moment your app has more than two or three distinct "areas" or "pages," the benefits of colocation start to pay off. It might feel like a tiny bit more work upfront, but I promise it saves you from a massive, painful refactor down the line. If you're starting a new project you expect to grow, I'd urge you to start with a feature-based structure from day one.
What about Next.js and its
appdirectory? How does this pattern fit in?Great question! The Next.js
approuter actually encourages a feature-based structure by its very nature. You're already grouping pages, layouts, and components by route segments. This pattern complements it perfectly. Yoursrc/featuresdirectory can hold the complex business logic, hooks, and API layers, while theappdirectory primarily handles routing and composing the UI using components imported from your features. For example,app/dashboard/products/page.tsxwould import and use components fromsrc/features/products. They work together beautifully.
Isn't this over-engineering for a small project?
You know, it can be! If you're building a simple landing page or a prototype that you know will be thrown away, then yes, this is probably overkill. Stick with a simple structure and don't sweat it. But the term "small project" can be deceptive. Many "small" projects have a funny habit of becoming large, critical applications over time. My advice is to be honest about the project's potential. If there's a good chance it will grow, adopting a scalable structure early is an investment that will pay for itself a hundred times over.