DEV Community

Cover image for A Practical React Project Structure
Nicholas Fane
Nicholas Fane

Posted on

A Practical React Project Structure

Keep your app predictable, modular, and scalable from day one. This structure combines Bulletproof React ideas with a simple feature-first layout that avoids domain specifics and stays easy to navigate.

Goals

  • Clear boundaries: Separate generic vs feature-specific code
  • Fast imports: Use path aliases, rather than needlessly traversing the file structure in your code
  • Repeatable pattern: “Copy the structure,” not reinvent it

Base Layout (app-wide)

Use a small set of top-level folders and stick to them.
Note that this structure exists under your 'src' folder, as the root of your project may differ based on what framework you are using (I am using Vite)

src/
  app/            # App entry, providers, global styles, bootstrapping
  routes/         # Route files (code-split where possible)
  components/     # Highly generic, reusable UI only (no feature logic)
  features/       # Feature modules (each self-contained)
  hooks/          # Reusable cross-feature hooks
  utils/          # Pure utilities and helpers
  services/       # Integrations with external systems
  types/          # Global/shared TypeScript types
  store/          # Store logic and state management
  assets/         # Static assets
Enter fullscreen mode Exit fullscreen mode
  • app: central wiring (providers, theming, error boundaries).
  • routes: route-level code splitting and lazy loading.
  • components: generic-only; if you feel tempted to add feature logic here, it belongs in features/.
  • features: replicate a mini version of the base structure per feature (see below).
  • store: generic state management code, like bootstrapping/exporting the main provider. Specific reducers/selectors will usually exist in their respective feature.
  • services: any code required to connect with an external system
  • hooks/utils/types: shareable and framework-agnostic where possible.

Path Aliases

Keep imports short and consistent:

import { Button } from "@components/ui/Button";
import { loadUser } from "@features/user/services/api";
import { formatDate } from "@utils/date";
Enter fullscreen mode Exit fullscreen mode
  • Common aliases: @/, @components/, @features/, @hooks/, @utils/.

Your tsconfig paths property will probably need to look something like this

    "paths": {
      "@/*": ["./src/*"],
      "@app/*": ["./src/app/*"],
      "@components/*": ["./src/components/*"],
      "@features/*": ["./src/features/*"],
      "@hooks/*": ["./src/hooks/*"],
      "@types/*": ["./src/types/*"],
      "@utils/*": ["./src/utils/*"],
      "@store/*": ["./src/store/*"],
      "@services/*": ["./src/services/*"],
      "@assets/*": ["./src/assets/*"]
    }
Enter fullscreen mode Exit fullscreen mode

Features Folder Pattern

Each feature is a self-contained slice. Replicate the same internal pattern for every feature to scale cleanly. Below is an example, but the core idea is that you can/should copy the structure defined above to build suit the feature.

src/features/<feature-name>/
  components/     # Feature UI (dumb-first; compose small pieces, but don't stress about making stuff 'overly-generic')
  hooks/          # Feature-specific hooks
  services/       # Data fetching, adapters, caching, side effects
  types/          # Feature-only types/interfaces
  index.ts        # Public surface (barrel exports)
Enter fullscreen mode Exit fullscreen mode
  • Keep feature concerns inside the feature. Avoid reaching into other features. If you find you need to import a hook from another feature, then congratulations, you've just found a practical use case to create a generic hook
  • Export only what the rest of the app should use from index.ts.
  • If something becomes generic across features, promote it to @components/, @hooks/, or @utils/.

Bulletproof React Principles (essentials)

This structure is derived from Bulletproof React, which is a great foundation to expand upon depending on your needs.

Some of the core concepts are below:

  • Feature-first: colocate logic, UI, and types by feature.
  • Barrel files (index.ts): define clear public APIs per folder.
  • Type safety: strict TypeScript, no any; define props and service types.
  • Early returns over nesting: keep components/functions shallow.
  • Small, focused modules: single responsibility; extract when files grow.
  • Pure utilities in @utils/: deterministic and easy to test.

How to Add a New Feature (repeatable)

  1. Create src/features/<feature-name>/.
  2. Add components/, hooks/, services/, types/, index.ts (or add the required folders at the time you need them).
  3. Build feature-specific UI in components/, stateful logic in hooks/, side effects in services/.
  4. Export the minimal public surface from index.ts.
  5. Wire it into routes/.
  6. Test at the feature boundary; add E2E coverage if it affects user flows.

What Goes Where (quick rules)

  • Generic UI: @components/
  • Feature UI/logic: @features/<feature>/
  • Reusable hooks: @hooks/
  • Pure helpers: @utils/
  • Shared types: types/
  • Routes & splitting: routes/
  • App bootstrapping: app/

Conclusion

I hope you found this helpful. Feel free to share your thoughts on what works and what doesn't with this structure. Otherwise, happy developing and good luck on your next project.

Find me here:
https://www.linkedin.com/in/nicholas-fane-06205897/
https://github.com/NickFane/

Top comments (0)