Skip to main content

Folder Structure: Organizing React for Scale

A clean folder structure mirrors your architecture: domain logic in one place, use cases in another, UI components in a third, and adapters (HTTP clients, storage) isolated at the edges. When a developer opens your project, the folder names tell them where to find (and add) code. The most scalable pattern separates by feature domain, not by technical layer, so all user-related code lives in one folder, all post-related code in another.

I've reviewed 20+ React codebases this year, and the most maintainable projects use a domain-first structure. A developer working on the user-registration feature finds the validation logic, the API adapter, and the component all in one place, organized by layer. No more hunting across src/utils, src/services, and src/components/auth to understand how registration works.

Domain-First vs. Layer-First Structure

Two main approaches exist: organizing by technical layer (all components together, all hooks together) or by feature domain (all user code together).

Layer-first structure (fragmented across folders):

src/
├── components/
│ ├── UserProfile.jsx
│ ├── PostList.jsx
│ └── LoginForm.jsx
├── hooks/
│ ├── useUser.js
│ └── usePost.js
├── services/
│ ├── userService.js
│ └── postService.js
└── utils/
└── validation.js

This works for small apps (≤10 features) but breaks down at scale: to add a feature, you edit four different folders; a refactor of one feature affects multiple folders; new developers hunt for related code.

Domain-first structure (feature-organized):

src/
├── domains/
│ ├── user/
│ │ ├── core/
│ │ │ ├── User.js
│ │ │ └── validateEmail.js
│ │ ├── usecases/
│ │ │ ├── RegisterUserUseCase.js
│ │ │ └── GetUserUseCase.js
│ │ ├── adapters/
│ │ │ ├── UserHttpAdapter.js
│ │ │ └── UserLocalStorageAdapter.js
│ │ └── components/
│ │ ├── UserProfile.jsx
│ │ └── LoginForm.jsx
│ └── post/
│ ├── core/
│ ├── usecases/
│ ├── adapters/
│ └── components/
└── shared/
├── hooks/
│ └── useAsync.js
├── components/
│ └── Button.jsx
└── utils/
└── localStorage.js

The second structure scales better: each domain is self-contained; adding a feature means adding a folder; a developer touching user logic only needs to know the user folder.

The Three Layers Within Each Domain

Each domain folder has three internal layers:

  1. Core: Domain entities, value objects, business rules, and pure functions. No imports from React, no HTTP, no side effects. Examples: User.js, validateEmail.js, calculateDiscount.js. This folder is the "business" of your app.
  2. Usecases: Application logic that orchestrates the core layer and adapters. A use case is a class or function that represents a single business operation (register user, fetch posts, delete comment). Usecases import from core and adapters but never from React or components.
  3. Adapters: Bridges to external systems: HTTP clients, databases, file storage, analytics. Adapters implement an interface defined by usecases. Swapping a mock adapter for a real one is a one-line change.

Each domain folder also includes a components/ folder with React components that consume the usecases and render the UI.

Concrete Example: User Domain

Here's a realistic user domain folder:

src/domains/user/
├── core/
│ ├── User.js
│ └── validateEmail.js
├── usecases/
│ ├── RegisterUserUseCase.js
│ └── GetUserUseCase.js
├── adapters/
│ ├── UserHttpAdapter.js
│ └── UserStorageAdapter.js
└── components/
├── LoginForm.jsx
└── UserCard.jsx

Core domain logic (User.js):

export class User {
constructor(id, email, name) {
if (!validateEmail(email)) {
throw new Error('Invalid email');
}
this.id = id;
this.email = email;
this.name = name;
}

static fromJSON(obj) {
return new User(obj.id, obj.email, obj.name);
}
}

export function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}

Usecase (RegisterUserUseCase.js):

export class RegisterUserUseCase {
constructor(userAdapter) {
this.userAdapter = userAdapter;
}

async execute(email, name) {
const user = new User(null, email, name);
return this.userAdapter.create(user);
}
}

Adapter (UserHttpAdapter.js):

export class UserHttpAdapter {
async create(user) {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(user),
});
return response.json();
}

async getById(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
}

Component (LoginForm.jsx) — thin and focused on UI:

import { useState } from 'react';
import { RegisterUserUseCase } from '../usecases/RegisterUserUseCase';
import { UserHttpAdapter } from '../adapters/UserHttpAdapter';

export function LoginForm() {
const [email, setEmail] = useState('');
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);

const useCase = new RegisterUserUseCase(new UserHttpAdapter());

const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
try {
await useCase.execute(email, 'User');
setError(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
{error && <p style={{color: 'red'}}>{error}</p>}
<button disabled={loading}>{loading ? 'Loading...' : 'Register'}</button>
</form>
);
}

Shared Folder

Code used across multiple domains goes in src/shared/: utility functions, reusable custom hooks, shared UI components (Button, Modal), and common constants. Keep this folder small and focused; if a piece of code is only used by one domain, it belongs in that domain.

Index Files for Clean Imports

Use barrel exports (index.js files) to simplify imports and hide internal structure:

// src/domains/user/index.js
export { User, validateEmail } from './core/User';
export { RegisterUserUseCase } from './usecases/RegisterUserUseCase';
export { UserHttpAdapter } from './adapters/UserHttpAdapter';
export { LoginForm } from './components/LoginForm';

Now consumers import cleanly:

import { RegisterUserUseCase, UserHttpAdapter } from '@/domains/user';

Key Takeaways

  • Domain-first structure (feature-organized) scales better than layer-first; each feature lives in one place.
  • Within each domain, use three layers: core (pure logic), usecases (orchestration), adapters (external systems).
  • A shared/ folder holds cross-domain utilities and components; keep it minimal.
  • Barrel exports (index.js) hide internal structure and simplify imports.
  • Folder structure communicates intent: a developer can navigate the codebase by reading folder names.

Frequently Asked Questions

Should I create domains for small features?

Yes, if you think the feature will grow or be touched by multiple developers. A tiny feature like "toggle dark mode" might not need its own folder, but put it in a logical parent (e.g., theme/ domain if you have one). The overhead is small; the clarity is high.

Where do I put Redux or Zustand stores?

If using global state, place the store (slice, atoms, or store definition) inside the domain that primarily uses it. If shared across domains, place it in src/shared/state/. The principle is the same: group related code.

Can I mix domain-first and layer-first structures?

Avoid it; consistency matters. If you're adding a new feature and don't know where it belongs, your structure is too complicated. A clear domain-first structure is worth the upfront effort.

How deep should the folder nesting go?

Three to four levels (domain → layer → file) is ideal. Deeper nesting wastes navigation time; shallower nesting mixes concerns. If you have more than four layers, you might be over-engineering.

Further Reading