Как правильно структурировать frontend-проект: папки, модули и здравый смысл
Хорошая структура frontend-проекта нужна не для красоты дерева папок. Она помогает быстро находить код, разделять ответственность и не превращать приложение в набор файлов utils.js, helpers.js и components, в которых спустя полгода уже никто ничего не понимает. Разберём, как организовать проект так, чтобы структура росла вместе с приложением, а не мешала ему.

Почти каждый frontend-проект начинается примерно одинаково.
Есть папка components, рядом pages, немного utils, пара API-запросов — и всё выглядит аккуратно. Затем проект начинает расти.
В components уже 60 файлов. В utils лежит всё — от форматирования даты до проверки токена авторизации. Один компонент страницы занимает 800 строк и одновременно загружает данные, проверяет права пользователя, открывает модальное окно и рендерит таблицу.
А потом появляется новый разработчик и задаёт простой вопрос:
Где здесь находится логика оформления заказа?
И начинается экскурсия по проекту.
На практике хорошая структура frontend-проекта должна отвечать именно на такие вопросы. Разработчик должен примерно понимать, где искать конкретную функциональность, ещё до того, как открыл поиск по всему проекту.
При этом есть важный нюанс: универсальной структуры, которая подходит абсолютно всем приложениям, не существует.
Одностраничный лендинг, административная панель и большой маркетплейс требуют совершенно разной организации кода. Поэтому задача не в том, чтобы найти идеальную структуру из статьи и скопировать её. Задача — понять принципы, по которым проект можно структурировать осознанно.
Начинайте со структуры, которая соответствует размеру проекта
Одна из самых распространённых ошибок — пытаться применить архитектуру огромного корпоративного приложения к небольшому проекту из пяти страниц.
В результате вместо разработки начинается обслуживание архитектуры.
Допустим, мы создаём небольшое приложение на React.
На старте структура вполне может выглядеть так:
src/
├── components/
├── pages/
├── services/
├── hooks/
├── utils/
├── styles/
├── App.jsx
└── main.jsx
И в этом нет ничего плохого.
components содержит переиспользуемые компоненты, pages — страницы, services — работу с API, hooks — пользовательские хуки, а utils — небольшие вспомогательные функции.
Для небольшого проекта такая структура понятна и практически не требует объяснений.
Проблемы начинаются не здесь.
Проблемы начинаются тогда, когда проект вырос в несколько раз, а структура осталась прежней.
Например:
components/
├── Header.jsx
├── Footer.jsx
├── Button.jsx
├── LoginForm.jsx
├── ProductCard.jsx
├── CartItem.jsx
├── CheckoutForm.jsx
├── UserAvatar.jsx
├── OrderTable.jsx
├── ProductFilters.jsx
└── ещё 70 файлов
Формально всё правильно — это действительно компоненты.
Но структура проекта больше ничего не говорит о самом приложении.
Поэтому по мере роста проекта имеет смысл переходить от организации кода по типу файла к организации по смыслу или функциональности.
Структурируйте проект вокруг функциональности
Представим интернет-магазин.
В приложении есть:
- авторизация;
- каталог;
- корзина;
- оформление заказа;
- профиль пользователя.
Логично, что каждая такая функциональность состоит не только из компонентов.
У корзины могут быть свои:
- компоненты;
- хуки;
- API-запросы;
- типы;
- функции;
- состояние.
Вместо того чтобы разбрасывать их по всему проекту, можно хранить код рядом.
Например:
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── api/
│ │ ├── hooks/
│ │ └── auth.service.js
│ │
│ ├── cart/
│ │ ├── components/
│ │ ├── api/
│ │ ├── hooks/
│ │ └── cart.store.js
│ │
│ └── checkout/
│ ├── components/
│ ├── api/
│ └── hooks/
│
├── pages/
├── shared/
└── app/
Теперь, если разработчику нужно изменить корзину, он практически сразу понимает, куда идти.
Вот это уже важное свойство хорошей архитектуры: код, связанный одной задачей, находится рядом.
Такой подход часто называют feature-based структурой — структурой по функциональности.
На практике для средних и крупных приложений он обычно масштабируется заметно лучше, чем огромные глобальные папки components, hooks и utils.
Разделяйте глобальный и локальный код
Одна из самых сложных вещей при организации frontend-проекта — понять, что действительно является общим.
Возьмём компонент кнопки.
<Button>Купить</Button>
Если эта кнопка используется по всему приложению, логично положить её в общий UI:
shared/
└── ui/
└── Button/
Но теперь представим компонент:
<AddProductToCartButton />
Он тоже является кнопкой.
Но относится ли он к общим компонентам?
Скорее всего, нет.
Он содержит конкретную бизнес-логику: добавляет товар в корзину. Поэтому логичнее хранить его внутри функциональности корзины или товара.
Например:
features/
└── cart/
└── components/
└── AddProductToCartButton.jsx
Здесь работает довольно простой принцип.
Общий код ничего не должен знать о конкретной бизнес-функции.
Обычный Button ничего не знает о корзине.
А AddProductToCartButton уже знает.
Именно здесь чаще всего допускают ошибку. Разработчик видит переиспользуемый React-компонент и автоматически отправляет его в shared.
Через несколько месяцев shared превращается в склад всего проекта.

Как может выглядеть структура реального проекта
Для приложения среднего размера можно использовать примерно такую организацию:
src/
├── app/
│ ├── router/
│ ├── providers/
│ ├── store/
│ └── App.jsx
│
├── pages/
│ ├── HomePage/
│ ├── ProductPage/
│ ├── CartPage/
│ └── ProfilePage/
│
├── features/
│ ├── auth/
│ ├── cart/
│ ├── product-search/
│ └── checkout/
│
├── entities/
│ ├── user/
│ ├── product/
│ └── order/
│
├── shared/
│ ├── api/
│ ├── ui/
│ ├── hooks/
│ ├── lib/
│ ├── config/
│ └── styles/
│
└── main.jsx
Разберём логику этой структуры.
app
Здесь находится код, который отвечает за запуск и конфигурацию всего приложения.
Например:
app/
├── router/
├── providers/
├── store/
└── App.jsx
Сюда можно положить настройку маршрутизации, глобального состояния, контекстов и других вещей уровня всего приложения.
При этом бизнес-логике здесь обычно делать нечего.
Если внутри App.jsx появляется функция вроде:
async function createOrder() {
// ...
}
скорее всего, она находится не на своём месте.
pages
Папка pages содержит страницы приложения.
Но здесь есть важный момент.
Страница не обязательно должна содержать всю связанную с ней логику.
Например:
import { ProductList } from '@/features/product-list';
import { ProductFilters } from '@/features/product-filters';
export function CatalogPage() {
return (
<>
<h1>Каталог</h1>
<ProductFilters />
<ProductList />
</>
);
}
Страница в таком случае работает как точка сборки.
Она объединяет несколько частей приложения, но сама остаётся относительно простой.
Это намного удобнее, чем страница на 1500 строк, которую страшно открывать перед релизом.
features
Здесь находится пользовательская функциональность.
Например:
features/
├── auth/
├── add-to-cart/
├── product-search/
├── checkout/
└── change-password/
Можно попробовать посмотреть на feature как на действие пользователя.
Пользователь может:
- авторизоваться;
- добавить товар в корзину;
- найти товар;
- оформить заказ;
- изменить пароль.
Это хороший ориентир при разделении проекта.
Например:
features/
└── auth/
├── api/
│ └── login.js
├── components/
│ └── LoginForm.jsx
├── hooks/
│ └── useAuth.js
└── index.js
Весь код авторизации находится рядом.
Удалить функциональность или перенести её в другой проект становится намного проще.
entities
Этот слой имеет смысл в более крупных проектах.
Здесь находятся основные сущности приложения.
Для интернет-магазина это могут быть:
entities/
├── product/
├── user/
├── cart/
└── order/
Например, сущность товара может содержать:
product/
├── api/
├── model/
├── ui/
└── index.js
Компонент ProductCard вполне может относиться именно к сущности product, потому что он представляет товар.
А компонент AddToCartButton — уже к отдельной функциональности.
Разница кажется небольшой, но она помогает избежать ситуации, когда всё приложение постепенно превращается в одну огромную папку features.
Для маленького проекта отдельный слой entities может быть избыточным. Добавлять его просто потому, что так делают в больших архитектурах, я бы не стал.
Архитектура должна решать проблему, а не создавать её заранее.
shared
Здесь находится действительно общий код.
Например:
shared/
├── api/
├── ui/
├── hooks/
├── lib/
├── config/
└── styles/
В ui могут находиться:
Button
Input
Modal
Spinner
Container
В lib:
formatPrice.js
formatDate.js
debounce.js
В api — базовая настройка HTTP-клиента.
Например:
const API_URL = import.meta.env.VITE_API_URL;
export async function apiRequest(url, options = {}) {
const response = await fetch(`${API_URL}${url}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
}
А уже конкретный запрос получения товаров лучше хранить ближе к товарам:
import { apiRequest } from '@/shared/api';
export function getProducts() {
return apiRequest('/products');
}
Таким образом, общий HTTP-механизм находится в shared, а бизнес-запрос — в соответствующем модуле.
Не превращайте utils в мусорное ведро
Папка utils появляется почти в каждом проекте.
Поначалу там действительно лежат полезные функции:
utils/
├── formatDate.js
└── formatPrice.js
Через несколько месяцев структура иногда выглядит так:
utils/
├── formatDate.js
├── formatPrice.js
├── loginUser.js
├── checkPermissions.js
├── buildCart.js
├── calculateDiscount.js
├── parseOrder.js
└── getSomething.js
Проблема в том, что слово utils практически ничего не говорит о назначении кода.
Если функция используется только корзиной, пусть она находится внутри корзины.
Например:
features/
└── cart/
└── lib/
└── calculateCartTotal.js
А в общий shared/lib имеет смысл переносить только действительно универсальные функции.
Например:
export function formatPrice(value) {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
}).format(value);
}
Эта функция ничего не знает о конкретной странице или функциональности, поэтому её вполне можно считать общей.
Используйте публичный API модуля
Ещё одна полезная практика — не импортировать внутренние файлы модуля напрямую из разных частей приложения.
Представим структуру:
features/
└── auth/
├── components/
│ └── LoginForm.jsx
├── hooks/
│ └── useAuth.js
└── index.js
Вместо:
import { LoginForm } from '@/features/auth/components/LoginForm';
можно сделать:
// features/auth/index.js
export { LoginForm } from './components/LoginForm';
И использовать:
import { LoginForm } from '@/features/auth';
На первый взгляд разница небольшая.
Но теперь внешний код не знает внутреннюю структуру модуля.
Вы можете потом перенести компонент:
components/LoginForm.jsx
в:
ui/LoginForm.jsx
и изменить только один файл index.js.
Остальное приложение продолжит работать.
По сути, index.js становится публичным интерфейсом модуля.
Это примерно та же идея, что используется в backend-разработке: наружу показываем только то, что действительно разрешено использовать.
Следите за направлением зависимостей
Структура папок сама по себе ничего не гарантирует.
Можно создать красивое дерево проекта, а затем связать его десятками циклических импортов.
Допустим, есть:
shared
entities
features
pages
app
Логика зависимостей может идти примерно снизу вверх.
shared ничего не знает о features.
entities могут использовать shared.
features могут использовать entities и shared.
pages собирают features.
app запускает всё приложение.
Условно:
app
↓
pages
↓
features
↓
entities
↓
shared
А вот обратная зависимость выглядит подозрительно.
Например:
// shared/ui/Button.jsx
import { useCart } from '@/features/cart';
Теперь общий компонент кнопки зависит от конкретной функциональности корзины.
Формально код может работать.
Архитектурно получилось наоборот: фундамент приложения знает о верхнем этаже.
Обычно здесь всё начинает ломаться не сразу. Проблемы появляются позже, когда компонент пытаются переиспользовать в другом месте.

Пример: как не стоит делать
Представим компонент страницы товара:
export function ProductPage() {
// загрузка товара
// проверка пользователя
// работа с корзиной
// избранное
// аналитика
// модальное окно
// форматирование цены
// отправка API-запросов
return (
<div>
{/* 600 строк JSX */}
</div>
);
}
Такой компонент часто появляется постепенно.
Никто специально не пишет файл на 1000 строк. Просто сначала там 150 строк, потом добавили фильтр, затем аналитику, потом модальное окно — и вот уже страшно что-либо трогать.
Лучше разделить ответственность:
pages/
└── ProductPage/
entities/
└── product/
└── ui/
└── ProductCard/
features/
├── add-to-cart/
├── add-to-favorites/
└── product-gallery/
Тогда страница становится точкой композиции:
import { ProductInfo } from '@/entities/product';
import { AddToCartButton } from '@/features/add-to-cart';
import { AddToFavoritesButton } from '@/features/add-to-favorites';
export function ProductPage() {
return (
<main>
<ProductInfo />
<AddToCartButton />
<AddToFavoritesButton />
</main>
);
}
Конечно, реальный компонент будет сложнее.
Но сама идея остаётся: страница должна скорее собирать функциональность, чем реализовывать абсолютно всё внутри себя.
Не создавайте папку на каждый файл
Есть и противоположная проблема.
Иногда желание сделать архитектуру идеальной приводит к такой структуре:
Button/
├── components/
│ └── Button/
│ ├── ui/
│ │ └── Button.jsx
│ └── index.js
└── index.js
Чтобы найти компонент из 15 строк, приходится пройти небольшое архитектурное приключение.
Если модуль содержит один файл, совершенно нормально оставить его одним файлом:
ui/
└── Button.jsx
Когда модуль вырастет, структуру всегда можно изменить.
Это вообще один из самых полезных принципов frontend-архитектуры:
Не проектируйте заранее сложность, которой пока нет.
Хорошая архитектура должна позволять проекту расти постепенно.
Используйте единый стиль именования
Структура быстро становится неудобной, если разные части проекта называются по-разному.
Например:
ProductCard/
product-list/
User_Profile/
cartItem/
Технически ничего страшного.
Но проект начинает выглядеть так, будто его собирали четыре команды, которые никогда друг с другом не разговаривали.
Выберите один подход.
Например, папки компонентов:
ProductCard/
UserProfile/
CartItem/
Обычные файлы:
formatPrice.js
getProducts.js
useAuth.js
Компоненты:
ProductCard.jsx
LoginForm.jsx
Хуки:
useAuth.js
useDebounce.js
Главное здесь даже не конкретный стиль.
Главное — последовательность.
Когда нужно менять структуру проекта
Не стоит ждать момента, когда проект окончательно превратится в археологический объект.
Есть несколько довольно очевидных сигналов.
Вы постоянно используете глобальный поиск, потому что не понимаете, где находится нужная логика.
Одна папка содержит десятки или сотни файлов.
Чтобы изменить корзину, приходится открывать файлы в пяти несвязанных директориях.
Появляются циклические зависимости.
Один компонент начинает отвечать сразу за несколько независимых задач.
Папки вроде utils, helpers и common растут быстрее остальных.
Если вы регулярно сталкиваетесь с такими ситуациями, скорее всего, структура проекта уже не соответствует его размеру.
Нужно ли использовать Feature-Sliced Design
Когда речь заходит об архитектуре frontend-приложений, часто появляется Feature-Sliced Design.
Сам подход интересный: он предлагает разделять приложение на уровни и чётче контролировать зависимости между ними.
Например:
app
pages
widgets
features
entities
shared
Для крупных проектов такая формализация действительно может быть полезной.
Но я бы не советовал воспринимать любую архитектурную методологию как обязательную инструкцию.
Если у вас небольшая административная панель на десять экранов, полноценная многоуровневая архитектура может оказаться сложнее самого приложения.
А вот если проект большой, над ним работает команда и количество бизнес-функций постоянно растёт, строгие правила зависимостей становятся гораздо полезнее.
Сначала должна появиться проблема.
Потом — архитектурное решение.
Не наоборот.
Какую структуру выбрать на старте
Для небольшого проекта я бы спокойно начал с простой структуры:
src/
├── components/
├── pages/
├── hooks/
├── services/
├── utils/
└── styles/
Когда приложение станет больше, можно перейти к функциональному разделению:
src/
├── app/
├── pages/
├── features/
└── shared/
Если проект действительно крупный и появляется множество бизнес-сущностей:
src/
├── app/
├── pages/
├── widgets/
├── features/
├── entities/
└── shared/
Главное — не воспринимать эти варианты как три архитектуры, между которыми нужно выбрать одну раз и навсегда.
Это скорее этапы роста.
На практике хорошая структура frontend-проекта обычно эволюционирует вместе с кодовой базой.
Вывод
Правильная структура frontend-проекта — это не красивое дерево директорий из GitHub.
Главная проверка намного проще.
Можете ли вы примерно понять, где находится нужный код, не открывая поиск?
Можно ли изменить одну функциональность, не затрагивая половину приложения?
Понятно ли новому разработчику, где заканчивается корзина и начинается оформление заказа?
Если да — структура работает.
Для небольших проектов не бойтесь использовать простые components, pages и services. Когда приложение растёт, группируйте код по функциональности, держите связанную логику рядом и отделяйте общий код от бизнес-кода.
И главное — не стройте архитектуру ради архитектуры.
Папки не делают проект хорошим автоматически. Они всего лишь помогают разработчикам ориентироваться в коде.
А значит, лучшая структура — не самая сложная, а та, в которой через полгода вы сможете открыть проект и быстро понять, что здесь вообще происходит.
