Here are five lessons that completely changed how I structure React projects
A common mistake many developers make (including my past self) is splitting everything by type:
src/components/hooks/pages/utils/
That seems neat at first — until your app grows. Suddenly, you’re jumping across files and folders just to make one change.
A better way: group by feature. Keep everything related to a feature together — components, hooks, and API calls included.
src/features/auth/components/hooks/api/dashboard/components/hooks/api/
Some elements — buttons, modals, utility functions — are used everywhere.
Put these in a shared/ folder to avoid duplication and keep your code DRY (Don’t Repeat Yourself).
src/shared/components/hooks/utils/
This helps maintain consistency across your app and reduces future headaches.
Clear naming = clear thinking. Here’s what works well:
UserProfile.jsxuseAuth.js, formatDate.jsSmall details like this make your project easier to navigate — especially for new contributors.
Just like in backend development, frontend apps have layers too. Understanding them helps you keep your architecture clean and modular.
👉 Rule: Keep these layers decoupled.
my-app/│├── public/│ └── favicon.svg│├── src/│ ││ ├── app/ # Application bootstrap & global config│ │ ├── App.tsx│ │ ├── main.tsx│ │ ││ │ ├── layouts/│ │ │ ├── MainLayout.tsx│ │ │ ├── AuthLayout.tsx│ │ │ └── DashboardLayout.tsx│ │ ││ │ ├── providers/ # Global providers│ │ │ ├── QueryProvider.tsx│ │ │ ├── StoreProvider.tsx│ │ │ ├── ThemeProvider.tsx│ │ │ └── RouterProvider.tsx│ │ ││ │ ├── routes/ # Centralized routing│ │ │ ├── index.tsx│ │ │ ├── ProtectedRoute.tsx│ │ │ └── routeConfig.ts│ │ ││ │ └── store/ # Global state config│ │ ├── index.ts│ │ ├── rootReducer.ts│ │ └── middleware.ts│ ││ ├── shared/ # Truly reusable across features│ │ ├── components/ # Design system components│ │ │ ├── Button/│ │ │ │ ├── Button.tsx│ │ │ │ └── index.ts│ │ │ ├── Input/│ │ │ ├── Table/│ │ │ └── Modal/│ │ ││ │ ├── hooks/ # Generic hooks│ │ │ ├── useDebounce.ts│ │ │ ├── useToggle.ts│ │ │ └── useIntersection.ts│ │ ││ │ ├── utils/ # Pure functions (NO React)│ │ │ ├── date.ts│ │ │ ├── validation.ts│ │ │ └── formatCurrency.ts│ │ ││ │ ├── types/│ │ │ └── common.ts│ │ ││ │ ├── constants/│ │ │ └── index.ts│ │ ││ │ └── styles/ # Global styling│ │ ├── globals.css│ │ └── variables.css│ ││ ├── features/ # 🔥 Business features (CORE of system)│ ││ │ ├── auth/│ │ │ ├── api/│ │ │ │ └── authApi.ts│ │ │ ││ │ │ ├── model/│ │ │ │ ├── authSlice.ts│ │ │ │ ├── authSelectors.ts│ │ │ │ └── authTypes.ts│ │ │ ││ │ │ ├─ ─ ui/│ │ │ │ ├── LoginForm.tsx│ │ │ │ └── AuthLayout.tsx│ │ │ ││ │ │ ├── hooks/│ │ │ │ └── useAuth.ts│ │ │ ││ │ │ └── index.ts│ │ ││ │ ├── users/│ │ │ ├── api/│ │ │ ├── model/│ │ │ ├── ui/│ │ │ ├── hooks/│ │ │ └── index.ts│ │ ││ │ └── dashboard/│ │ ├── api/│ │ ├── model/│ │ ├── ui/│ │ └── index.ts│ ││ ├── entities/ # Domain entities (optional advanced layer)│ │ ├── user/│ │ │ ├── types.ts│ │ │ └── mapper.ts│ │ └── product/│ ││ ├── services/ # Infrastructure layer│ │ ├── http/│ │ │ ├── httpClient.ts│ │ │ ├── interceptors.ts│ │ │ └── errorHandler.ts│ │ ││ │ ├── storage/│ │ │ └── localStorage.ts│ │ ││ │ └── logger/│ │ └── logger.ts│ ││ ├── lib/ # Third-party wrappers│ │ ├── react-query.ts│ │ ├── dayjs.ts│ │ └── sentry.ts│ ││ ├── config/ # Runtime configs│ │ ├── env.ts│ │ ├── featureFlags.ts│ │ └── permissions.ts│ ││ ├── tests/ # Global test utilities│ │ ├── setupTests.ts│ │ ├── renderWithProviders.tsx│ │ └── mockServer.ts│ ││ ├── e2e/ # Playwright / Cypress tests│ │ ├── auth.spec.ts│ │ └── dashboard.spec.ts│ ││ └── index.d.ts # Global type declarations│├── .env├── .env.development├── .env.production│├── package.json├── tsconfig.json├── tsconfig.node.json├── vite.config.ts├── vitest.config.ts├── playwright.config.ts│├── .eslintrc.cjs├── .prettierrc├── .editorconfig├── .lintstagedrc├── .gitignore└── README.md
Simple, predictable, and scalable.
// vite.config.jsresolve: {alias: {'@app': '/src/app','@features': '/src/features','@shared': '/src/shared','@assets': '/src/assets',}}