Любой проект начинается с чистой папки и свежего create-app. Через полгода у UserCard зависимость на ProductList, сборка падает на циклических импортах, а новый SuperButton некуда положить: слишком жирный для UI-kit, слишком общий для одной страницы.
Вопрос не в красоте папок. Вопрос, где жить этому коду, чтобы через три месяца человек, который его не писал, не гадал. Папка components врёт первой. В ней кнопка, карточка пользователя, кусок чекаута и «временный» хук с прошлого квартала лежат как равные. Они не равные.
С тех пор как перевёл на Feature-Sliced Design первый крупный фронтенд, кладу так почти везде. Этот сайт собран слоями shared → entities → features → widgets → app. Фронт Slot-Me тоже. Даже небольшой PassWave держал FSD, чтобы не вырастить свалку за две недели. На LeadForge фронт снова так. После десятка таких раскладок вопрос «куда класть» перестаёт быть вкусовщиной и становится привычкой, как класть ключи на одну полку.
Слои
shared: инструментальный ящик. UI-примитивы, утилиты, клиенты. Не знает ни пользователя, ни заказ.entities: предметные сущности.User,Product,Order. Модель и минимальный кусок UI этой модели.features: пользовательские сценарии. Войти, добавить в корзину, переключить тему.widgets: крупные блоки из фич и сущностей. Шапка, сетка товаров.pages: страница целиком.app: провайдеры, роутинг, тема.
Золотое правило: импортируем сверху вниз. Страница может взять виджет. Виджет не имеет права импортировать страницу. Обратный импорт это красная карточка.
На этом сайте между слоями нельзя ходить вглубь файла. Только через публичный index.ts. ESLint режет @/shared/ui/button, пропускает @/shared/ui. Иначе через месяц у половины проекта появится роман с внутренностями кнопки. Потом кнопку нельзя сдвинуть, не разбудив корзину.
SuperButton в этой схеме либо примитив в shared/ui, если он ничего не знает про домен, либо кусок фичи, если умеет «оформить заказ». Третьего места нет, и это как раз хорошо. Некуда спрятать полубога.
Что это даёт руками
Задача раскладывается в «куда положить». Примитив в shared/ui, карточка сущности в entities, действие в features, композиция в widgets. Спорить на ревью «это хук или фича» всё ещё можно. Спорить «а есть ли у нас папка для этого» уже нельзя.
Нижние слои не знают о верхних. Кнопка из shared не тащит за собой корзину и роутер. Это скучное правило. Оно экономит больше нервов, чем любой манифест про масштабирование.
Новичок за день понимает планировку. Не потому что FSD волшебный, а потому что карта одна. Ownership тоже проще: фича лежит в одном месте, а не размазана по components, hooks и utils/helpers2.
Циклических зависимостей от этого меньше не магией, а тем, что обратный импорт просто запрещён. Если очень хочется, линт орёт сразу, а не через месяц на проде.
Онбординг ускоряется не потому что «архитектура современная». Новичок открывает дерево и видит ту же карту, что вчера видел на другом моём репозитории. Ревью тоже короче: спор «это хук или утилита» остаётся, спор «создаём ли мы ещё одну папку misc» умирает.
Типовые грабли
Всё пихать в features. Через месяц там свалка. Данные и модель сущности в entities. То, что живёт без бизнеса, в shared.
Бизнес-логику в shared. Тогда «отвёртка» внезапно знает, как считать скидку оптовику. shared должен оставаться стерильным. Иначе кнопка начинает импортировать заказ, а заказ через два файла снова кнопку.
Слои без правил. Если импорты не смотреть, FSD это декорация над той же папкой components. Линт и договорённость, что считается нарушением. Буквальность ради буквальности не нужна. Дисциплина нужна.
Мини-FSD
На маленьком проекте не надо устраивать парад из шести этажей. Хватает shared → features → pages. Перерастёте, добавите entities и widgets. На лендинге из двух экранов, недельном прототипе и коротком сольном цикле каркас часто дороже пользы. Там достаточно не врать себе папками.
FSD не религия. Это ответ на вопрос «куда класть», когда папка components уже врёт.



