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 tasksh1>
<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.π