How to Migrate a React Application to Feature-Based Folder Structure
A step-by-step guide to reorganizing a flat React codebase into feature-based folders without breaking your application
Most React applications start with a type-based folder structure: a components/ folder, a hooks/ folder, a utils/ folder, and a pages/ folder. This layout is intuitive when the project is small. Every developer knows where a new component goes. The structure requires almost no upfront thought.
The problem appears as the application grows. A components/ folder with 200 files has no navigable structure. Adding a new feature requires touching four separate folders. A developer new to the codebase cannot find where the checkout flow lives without searching. The structure that was convenient at the beginning has become a liability.
Feature-based folder organization solves this by grouping files by what they belong to rather than what type of file they are. This guide walks through migrating from type-based to feature-based organization incrementally, without a rewrite and without breaking the application.
Photo by Walls.io on Pexels
The Problem with Type-Based Folders
When all components live together regardless of which feature they serve, every change to one feature requires navigating to multiple folders. Adding authentication means creating files in components/, hooks/, services/, and potentially utils/. Modifying the checkout flow requires the same cross-folder navigation. Each context switch adds cognitive overhead, and the relationships between files become implicit rather than visible in the folder structure itself.
Type-based organization also makes it easy for feature-specific logic to leak into shared utilities. A hook written for the dashboard gets used in two other places. A utility function grows application-specific behavior. Over time, the boundary between feature code and shared code disappears, and the codebase becomes harder to reason about as a whole.
React's documentation recommends grouping by features or routes rather than by file type for projects that grow beyond small scale, noting that the right structure is the one that makes it easy to find files related to each other.
What the Target Structure Looks Like
A feature-based structure groups everything related to a feature in one place. Components, hooks, services, and types for the authentication flow all live under features/auth/. The same pattern applies to every feature. Shared utilities live in a shared/ folder that contains only code that genuinely has no feature affiliation.
src/
features/
auth/
components/
LoginForm.tsx
AuthGuard.tsx
hooks/
useAuth.ts
useLogin.ts
services/
auth.service.ts
types/
auth.types.ts
index.ts
dashboard/
components/
DashboardLayout.tsx
StatsPanel.tsx
hooks/
useDashboard.ts
services/
dashboard.service.ts
index.ts
shared/
components/
Button/
Button.tsx
Button.test.tsx
index.ts
Modal/
Input/
hooks/
useLocalStorage.ts
useDebounce.ts
utils/
format.ts
validation.ts
app/
router.tsx
providers.tsx
main.tsx
The index.ts file at the root of each feature is the feature's public API. Other parts of the application import from the feature's index file, not from individual files within the feature. This enforces an explicit boundary and makes it possible to reorganize internals without breaking imports elsewhere.
The Bulletproof React repository shows a production-scale implementation of this structure with a working demo application. It is the most useful reference for understanding what this organization looks like in practice across a realistic feature set.
Step 1: Audit Your Current Structure
Before moving any files, spend time understanding what you have. List every folder in your current structure and note which features each folder contains code for. In a flat structure, components/ will contain code for a dozen different features. That is not a problem to fix immediately - it is information to work with.
The goal of the audit is to identify your application's features as discrete units. Authentication is one feature. The shopping cart is another. The user profile section is another. Each feature should eventually have its own folder. Write down the list of features and roughly which existing files belong to each one.
Also identify which existing code is genuinely shared - used by three or more features without being specific to any of them. A button component, a modal wrapper, a date formatting utility are candidates for the shared/ folder. A user profile card that appears on the dashboard and the settings page is probably a feature component that belongs in a specific feature, not a shared component.
Step 2: Start with a New Feature
The safest way to begin a migration is to apply the new structure only to new code first. When you add a new feature, create it in a features/ folder using the target structure. Do not touch existing code.
This approach validates the structure in your codebase before committing to a full migration. After a month of adding new features under features/, you have a working example of the new structure to point to. The old structure is still intact. You have not introduced any risk to existing functionality. TypeScript path aliases can be set up to make imports from the new structure clean from the start.
When the new structure is established and working, begin moving existing features one at a time, starting with the feature that is causing the most friction or the one that has the clearest boundary.
Step 3: Create the Feature Index File
Each feature's index.ts file defines its public interface. This is the file that other features and the application router import from. It exports only the components, hooks, and types that are intended for use outside the feature. Internal implementation details are not exported.
// features/auth/index.ts
export { LoginForm } from './components/LoginForm';
export { AuthGuard } from './components/AuthGuard';
export { useAuth } from './hooks/useAuth';
export type { User, AuthState } from './types/auth.types';
// Internal services and helper hooks are NOT re-exported
The index file serves as documentation of the feature's contract. When another developer looks at it, they see immediately what the feature exposes for external use and what is internal. This distinction becomes important when you need to refactor internal implementation - as long as the public exports stay stable, nothing outside the feature breaks.
Step 4: Move Files One Feature at a Time
When migrating an existing feature, move all its files to the new location in a single commit. Partial migrations - where half of the authentication code is in components/ and half is in features/auth/ - create confusion about where to look and where to add new code.
Before moving files, update all imports to point to the new location. Then move the files. Then verify the application still builds and tests still pass. The React Testing Library test suite and Jest should catch any import breakages immediately. If tests are sparse, run the application manually and verify the migrated feature works correctly before moving to the next one.
File moves in version control are easier to track if you keep the move commit separate from any content changes. Move first, then make content edits in a follow-up commit. This keeps the diff readable and makes any issues introduced by the move easier to identify.
Step 5: Enforce Import Boundaries with ESLint
Once features have their own folders, import boundary rules prevent the cross-feature coupling that the structure is designed to eliminate. ESLint with the eslint-plugin-import package can enforce that features only import from other features through index files, and that shared utilities never import from feature folders.
{
"rules": {
"import/no-restricted-paths": [
"error",
{
"zones": [
{
"target": "./src/features/auth",
"from": "./src/features/dashboard",
"message": "Auth feature must not import from dashboard feature."
},
{
"target": "./src/shared",
"from": "./src/features",
"message": "Shared utilities must not import from feature folders."
}
]
}
]
}
}
These rules do not need to cover every combination. Start with the boundaries most likely to be violated. Direct feature-to-feature imports and shared-to-feature imports are the two patterns most worth preventing. Add rules incrementally as the structure stabilizes.
Prettier handles formatting consistently across the new structure. Vite path aliases (@/features/, @/shared/) keep import paths clean as the structure deepens.
Step 6: Decide What Belongs in Shared
The shared folder is the hardest part of the migration to get right. The temptation is to put anything used by more than one feature into shared, which recreates the original problem at a smaller scale.
The test for a shared component is whether it could be extracted into a standalone library and used in a different application. A button component can. A user profile card that knows about the application's user data model probably cannot.
Storybook is useful for validating shared component design. If you can write a Storybook story for a component using only generic props and callbacks - without importing anything from a feature folder - it belongs in shared. If writing the story requires feature-specific context, the component belongs in a feature.
State management tools like TanStack Query for server state and Zustand for client state both pair well with feature-based organization. TanStack Query hooks belong in feature service layers. Zustand slices can be defined in feature folders and composed at the application level.
Step 7: Handle the Router
The application router typically lives outside of any feature folder because it is what assembles the features into a running application. React Router or a similar library connects routes to feature components that are imported through each feature's index file.
The router file is a useful canary for the quality of the migration. If importing a component into the router requires digging into feature internals - importing from features/auth/components/LoginForm rather than from features/auth - the feature's index file is not complete. Clean router imports are a signal that the feature boundaries are working correctly.
Photo by Josh Sorenson on Pexels
Common Pitfalls
The most common pitfall is creating too many features too granularly. If you have a features/button-group/ folder, the feature definition is too narrow. Features should correspond to user-facing functionality, not implementation details.
The second pitfall is leaving the old structure in place alongside the new one indefinitely. A hybrid codebase with some features in the old structure and some in the new one requires developers to know which system a given piece of code uses. Set a migration timeline and complete it.
The third pitfall is using the shared folder as a second catch-all. If code does not obviously belong to a feature, resist the impulse to put it in shared. It may belong to an existing feature that has not been fully extracted yet, or it may indicate a feature that needs to be identified and named.
Migrating to a feature-based structure is incremental work, not a rewrite. Start with new features, migrate existing ones one at a time, and use tooling to enforce the boundaries once they are in place.
For the full context on why these structural choices matter for long-term maintainability, the guide on React application structure covers the principles alongside the folder patterns.
https://137foundry.com provides web development services including codebase architecture reviews for React applications at various stages of growth.

