When the project is small, we can easily "dump everything into one folder" and work. But as the application grows, the chaos in the structure begins to slow down development, complicate support, and hinder new team members.
In this article, I'll show you how I structure frontend projects to be scalable, predictable, and convenient for teamwork.
Principles
Before moving on to the structure, I always adhere to three rules::
- Explicit is better than implicit — one additional folder is better than magic with obscure imports.
- Features are more important than layers — instead of "/components", "/services", I try to highlight functional modules.
- Scalability from day one — even if the project is small, the structure should allow for growth without restructuring.
Basic project structure
src/
├── app/ # Application Level configuration
│ ├── providers/ # Context providers, themes, routers
│ ├── store/ # Global status
│ └── config/ # Constants, settings
├── entities/ # Entities (User, Product, Todo, etc.)
├── features/ # Business features (Login, Search, Cart, etc.)
├── pages/ # Pages (UI level routing)
├── shared/ # Reusable utilities, UI components, helpers
└── index.TSX # Entry Point
Example: entities
The entities/
stores models, APIs, and minimal components. For example, entities/todo/
:
entities/
└── todo/
├── api/
│ └── todoApi.ts # CRUD operations
├── model/
│ ├── types.ts # Entity Types
│ └── store.ts # Zustand/Redux status
└── ui/
└── TodoItem.tsx # Basic UI Component
// entities/todo/api/todoApi.ts
export const fetchTodos = async () => {
const res = await fetch("/api/todos");
return res.json();
};
Example: feature
A feature combines several entities to solve a problem. For example, features/TodoList/
:
features/
└── todoList/
├── ui/
│ └── TodoList.tsx
└── model/
└── hooks.ts # Local Hooks
// features/todoList/ui/TodoList.tsx
import { useEffect, useState } from "react";
import { fetchTodos } from "@/entities/todo/api/todoApi";
import { TodoItem } from "@/entities/todo/ui/TodoItem";
export function TodoList() {
const [todos, setTodos] = useState([]);
useEffect(() => {
fetchTodos().then(setTodos);
}, []);
return (
<div>
{todos.map((todo) => (
<TodoItem key={todo.id} {...todo} />
))}
</div>
);
}
Example: a Page
Pages collect features and entities into a ready-made screen.
pages/
└── home/
└── HomePage.tsx
// pages/home/HomePage.tsx
import { TodoList } from "@/features/todoList/ui/TodoList";
export function HomePage() {
return (
<main>
<h1>My tasks</h1>
<TodoList />
</main>
);
}
Shared — always at hand
shared/
contains everything that does not depend on a specific entity or feature:
shared/
├── ui/ # Buttons, inputs, mods
├── lib/ # Utilities, helpers
├── api/ # Basic HTTP client
// shared/api/http.ts
export async function http<T>(url: string, options?: RequestInit): Promise<T> {
const res = await fetch(url, options);
if (!res.ok) throw new Error("Network error");
return res.json();
}
Why it works
- It's easy for a new developer to navigate: Essentials → Features → Page.
- The code can be scaled: new features are added without revising the entire architecture.
-
shared/
remains compact and predictable, without leaking business logic.
Conclusion
This structure has grown from real projects, from small pet projects to applications with dozens of developers. It's not the only correct one, but it avoids the "spaghetti code" and makes it easier to maintain.
And how do you structure projects? Share your experience in the comments.👇
Top comments (0)