Feature-Sliced Design: архитектура фронтенд-проектов с нуля

Практический курс по архитектурной методологии FSD для фронтенд-разработчиков. Вы разберёте все слои архитектуры, правила организации кода и научитесь применять FSD в реальных проектах — с примерами кода и практическими заданиями.

1. Что такое FSD и зачем он нужен

Что такое FSD и зачем он нужен

Представьте: вы открываете проект, которому год. В папке components — 200 файлов. Половина называется Card, CardNew, CardV2, CardFinal. Хочешь найти логику авторизации — она размазана между hooks/useAuth.ts, services/authService.ts и components/LoginForm. Хочешь добавить новую фичу — боишься, потому что непонятно, что сломается. Знакомо? Именно эту боль решает Feature-Sliced Design.

Проблема, которую решает FSD

Большинство фронтенд-проектов начинаются одинаково: папки components, pages, hooks, utils. Это работает, пока проект маленький. Но когда команда растёт, фич становится больше, а требования меняются — структура начинает работать против вас.

Конкретные симптомы:

  • Новый разработчик не может понять, куда добавить код, не спросив коллегу
  • Изменение одного компонента ломает три других в неожиданных местах
  • Один и тот же бизнес-объект (например, «пользователь») описывается в пяти разных местах
  • Импорты превращаются в спагетти: components импортирует из pages, pages из hooks, hooks из components
  • Это называют "big ball of mud" — архитектурный антипаттерн, когда у кода нет структуры, только хаос, который нарастает со временем.

    !Сравнение хаотичной структуры проекта и структуры по FSD

    Что такое Feature-Sliced Design

    > Feature-Sliced Design (FSD) — это архитектурная методология для фронтенд-приложений, которая организует код вокруг бизнес-функциональности, а не технических деталей. > > feature-sliced.design

    Проще говоря: вместо того чтобы складывать файлы по типу («все компоненты сюда, все хуки туда»), вы складываете их по смыслу — «всё, что связано с корзиной, — сюда; всё, что связано с профилем пользователя, — туда».

    FSD вводит два измерения организации кода:

  • Слои (layers) — горизонтальное разделение по уровню абстракции. Это фиксированный набор папок верхнего уровня: shared, entities, features, widgets, pages, processes, app.
  • Слайсы (slices) — вертикальное разделение внутри слоя по доменам. Например, внутри features будут слайсы add-to-cart, auth-by-email, edit-profile.
  • Внутри каждого слайса код делится на сегменты (segments) по технической роли: ui, model, api, lib, config.

    Аналогия из жизни: представьте большой офис. Слои — это этажи здания (бухгалтерия на 2-м, разработка на 3-м, менеджмент на 4-м). Слайсы — это отделы на каждом этаже. Сегменты — это конкретные рабочие места внутри отдела. Каждый знает, где что находится, и никто не ходит на чужой этаж без причины.

    Главное правило: зависимости только вниз

    Ключевой принцип FSD — однонаправленные зависимости. Слой может импортировать только из слоёв, которые находятся ниже него в иерархии. Никогда — из слоёв выше.

    Иерархия слоёв (от верхнего к нижнему):

    Это значит: features может использовать entities и shared, но не может импортировать из pages или app. Нарушение этого правила создаёт циклические зависимости и превращает проект в тот самый клубок спагетти.

    Пример нарушения, которое выглядит невинно, но разрушает архитектуру:

    Правильный подход — сущность ничего не знает о фичах. Фича сама знает о сущности:

    Публичный API слайса

    Второй фундаментальный принцип — каждый слайс общается с внешним миром только через публичный API. Это файл index.ts в корне слайса, который явно экспортирует только то, что разрешено использовать снаружи.

    Снаружи импортируем только через публичный API:

    Зачем это нужно? Если вы решите переписать внутреннюю реализацию фичи, публичный API останется прежним — и ничего снаружи не сломается. Это как розетка в стене: вам не важно, как проложена проводка внутри, важно, что штепсель подходит.

    Как FSD решает реальные проблемы

    Вернёмся к симптомам из начала статьи и посмотрим, что меняется с FSD.

    | Проблема без FSD | Решение в FSD | |---|---| | Непонятно, куда добавить код | Чёткая иерархия слоёв и слайсов даёт однозначный ответ | | Изменение ломает неожиданные места | Публичный API и изолированные слайсы ограничивают зону влияния | | Бизнес-объект описан в 5 местах | Сущность (entity) — единственный источник правды о домене | | Циклические импорты | Правило однонаправленных зависимостей делает циклы невозможными | | Долгий онбординг | Структура отражает бизнес-логику — новый разработчик понимает её интуитивно |

    Команда из flaton.systems описывает это так: они адаптировали FSD под свой page-based подход и зафиксировали ключевой принцип — «держим связанные элементы рядом до тех пор, пока они не потребуются в другом месте». Это и есть суть FSD: локальность кода до момента, когда переиспользование становится реальной необходимостью.

    Для каких проектов подходит FSD

    FSD — не серебряная пуля. Для лендинга из трёх страниц он избыточен. Но как только проект переходит определённый порог сложности, FSD начинает окупаться.

    FSD хорошо подходит, если:

  • Проект живёт дольше 3–6 месяцев и продолжает развиваться
  • В команде больше одного разработчика
  • Приложение имеет несколько доменных областей (пользователи, заказы, каталог и т.д.)
  • Требования часто меняются и нужна гибкость при рефакторинге
  • FSD работает с любым фреймворком — React, Vue, Angular, Svelte. Это методология, а не библиотека. Она не диктует стейт-менеджер или способ работы с API — только то, где должен жить код.

    Первый шаг: думать доменами, а не файлами

    Самое важное изменение при переходе на FSD — это не структура папок, а способ мышления. Прежде чем создавать файл, задайте себе вопросы:

  • К какому домену относится этот код? (пользователь, заказ, корзина?)
  • Это переиспользуемый примитив, бизнес-сущность или конкретное поведение пользователя?
  • Кто должен знать об этом коде — весь проект или только одна страница?
  • Ответы на эти вопросы укажут на правильный слой и слайс. В следующей статье мы разберём каждый слой подробно — с примерами кода и конкретными правилами для shared, entities и features.

    2. Слои архитектуры: Shared, Entities, Features

    Слои архитектуры: Shared, Entities, Features

    Три нижних слоя FSD — shared, entities, features — это фундамент всего приложения. Если они спроектированы правильно, верхние слои собираются как конструктор. Если нет — весь проект рассыпается. Разберём каждый из них до деталей: что туда кладут, что категорически нельзя, и как это выглядит в реальном коде.

    Слой Shared: переиспользуемые примитивы

    Shared — самый нижний слой. Он не знает ни о каком домене приложения. Здесь живёт всё, что можно скопировать в другой проект и оно будет работать без изменений.

    Типичное содержимое shared:

    Ключевой вопрос для проверки: «Зависит ли этот код от конкретного домена приложения?» Если да — он не принадлежит shared.

    Пример правильного компонента в shared/ui:

    Этот компонент ничего не знает о корзине, пользователях или заказах. Он просто кнопка.

    Пример базового API-клиента в shared/api:

    Частая ошибка: класть в shared хуки, которые обращаются к конкретному домену. Например, useCurrentUser — это не shared, это entities/user. В shared нет ничего про «текущего пользователя», «корзину» или «заказ».

    Слой Entities: бизнес-сущности

    Entities — слой бизнес-сущностей. Здесь живут объекты, которыми оперирует ваш бизнес: User, Product, Order, Cart. Каждая сущность — это отдельный слайс.

    Структура типичного слайса сущности:

    Разберём слайс entities/user полностью:

    !Структура слайса entities/user с сегментами ui, model, api и публичным API

    Обратите внимание: UserCard получает пользователя через пропсы. Компонент не знает, откуда пришли данные — из стора, из пропсов страницы или из запроса. Это делает его максимально переиспользуемым.

    Что НЕ должно быть в entities: бизнес-логика, которая относится к конкретному действию пользователя. Например, «добавить товар в корзину» — это не сущность, это фича. Сущность Product знает, как выглядит продукт и доступен ли он. Но она не знает, как его добавить в корзину.

    Пример бизнес-правила, которое правильно живёт в entities:

    Слой Features: пользовательские сценарии

    Features — самый насыщенный слой. Здесь живут законченные пользовательские сценарии: «войти по email», «добавить товар в корзину», «применить промокод», «загрузить аватар». Каждая фича — это минимальная единица поведения, которая имеет ценность для пользователя.

    Структура фичи:

    Полный пример фичи add-to-cart:

    !Интерактивная схема зависимостей между слоями shared, entities, features

    Как не превратить features в свалку

    Самая распространённая ошибка — делать фичи слишком большими. Например, создать features/profile и сложить туда всё, что связано с профилем:

    Правильный подход — одна фича, одна задача:

    Это даёт три практических преимущества. Во-первых, разные разработчики могут работать над разными фичами параллельно без конфликтов. Во-вторых, каждую фичу можно покрыть тестами изолированно. В-третьих, фичу можно переиспользовать на разных страницах — например, upload-avatar может появиться и в настройках профиля, и в онбординге.

    Ещё один важный момент: фичи одного слоя не должны импортировать друг друга. Если features/checkout нужна логика из features/apply-promo, это сигнал, что общую часть нужно опустить в entities или shared. Взаимодействие между фичами происходит через слои выше — через widgets или pages, которые их компонируют.

    В следующей статье разберём верхние слои: widgets, pages, processes и app — и посмотрим, как они собирают всё вместе в готовый интерфейс.

    3. Слои архитектуры: Widgets, Pages, Processes, App

    Слои архитектуры: Widgets, Pages, Processes, App

    Если shared, entities и features — это кирпичи, раствор и арматура, то widgets, pages, processes и app — это то, как из них строят здание. Верхние слои не создают новую логику с нуля, они компонуют то, что уже есть внизу. Разберём каждый слой и покажем, как они работают вместе.

    Слой Widgets: крупные блоки интерфейса

    Widgets — это самодостаточные блоки UI, которые объединяют несколько фич и сущностей в один законченный визуальный модуль. Виджет — это то, что пользователь воспринимает как единый элемент страницы: шапка сайта, боковая панель, карточка товара с кнопками действий, дашборд со статистикой.

    Ключевое отличие виджета от фичи: фича — это одно конкретное действие («добавить в корзину»), виджет — это визуальный блок, который может содержать несколько действий сразу.

    Структура типичного виджета:

    Пример виджета ProductCard, который объединяет сущность и несколько фич:

    Виджет знает о фичах и сущностях, но страница знает только о виджете. Это важно: страница не должна напрямую импортировать AddToCartButton — она получает его в составе ProductCard.

    Когда нужен виджет, а когда достаточно фичи? Простое правило: если блок UI используется на нескольких страницах и содержит больше одной фичи — это виджет. Если это одно конкретное действие — это фича.

    Слой Pages: страницы как точки входа

    Pages — слой, который соответствует маршрутам приложения. Каждая страница — это отдельный слайс, который компонует виджеты, фичи и сущности в готовый экран.

    > Главное правило страниц: они собирают, но не думают. Никакой сложной бизнес-логики на уровне страницы. > > purpleschool.ru

    Структура слоя pages:

    Пример страницы корзины — обратите внимание, насколько она «тупая»:

    А вот пример страницы с деталями продукта, где нужна небольшая логика загрузки:

    !Схема компоновки страницы из виджетов, фич и сущностей в FSD

    Слой Processes: сквозные сценарии

    Processes — необязательный слой, который появляется в FSD для многошаговых сценариев, пересекающих несколько страниц. Классические примеры: онбординг нового пользователя, оформление заказа (checkout flow), мастер настройки.

    Этот слой используется редко и только тогда, когда сценарий действительно «живёт» между страницами и не принадлежит ни одной из них.

    Пример стора для процесса оформления заказа:

    Важный нюанс: в современных версиях FSD слой processes считается устаревающим (deprecated). Официальная документация рекомендует либо поднимать такую логику в app, либо распределять по страницам, используя стейт-менеджер. Но в реальных проектах processes всё ещё встречается и вполне оправдан для сложных многошаговых флоу.

    Слой App: инициализация приложения

    App — самый верхний слой. Здесь живёт всё, что нужно для запуска приложения: провайдеры, роутинг, глобальные стили, инициализация сторов, обработка ошибок.

    Пример организации провайдеров:

    Как слои работают вместе

    Теперь посмотрим на полную картину. Когда пользователь открывает страницу /products/42, происходит следующее:

  • App — роутер определяет, что нужно рендерить ProductDetailsPage
  • PagesProductDetailsPage загружает данные и компонует виджеты
  • WidgetsProductCard объединяет отображение продукта и кнопки действий
  • FeaturesAddToCartButton обрабатывает клик и вызывает API
  • EntitiesProduct предоставляет тип данных и бизнес-правила (доступность)
  • SharedButton, apiClient используются на всех уровнях
  • Каждый слой делает ровно то, что должен. Никто не лезет в чужую зону ответственности. Именно поэтому изменение в AddToCartButton не затронет ProductDetailsPage — они общаются только через публичный API виджета.

    4. Правила и лучшие практики FSD

    Правила и лучшие практики FSD

    Знать структуру слоёв — это половина дела. Вторая половина — понимать неочевидные правила, которые превращают FSD из «красивой схемы на бумаге» в работающую архитектуру. Здесь собраны практические правила, типичные ловушки и способы их избежать.

    Правило публичного API — строже, чем кажется

    Публичный API через index.ts — это не просто «удобный реэкспорт». Это контракт. Нарушение этого правила — самая частая ошибка при внедрении FSD.

    Рассмотрим конкретный сценарий. Есть фича features/auth-by-email. Разработчик торопится и пишет:

    Через месяц другой разработчик переименовывает validation.ts в schemas.ts и переносит LoginForm в подпапку. Всё ломается. Причём в неожиданных местах.

    Правильный подход — всё, что нужно снаружи, явно экспортируется через index.ts:

    Практическое правило: если вы пишете импорт с путём глубже одного уровня внутри чужого слайса — это нарушение. Путь @/features/auth-by-email — правильно. Путь @/features/auth-by-email/ui/LoginForm — нарушение.

    Правило изоляции слайсов одного слоя

    Слайсы внутри одного слоя не должны импортировать друг друга. Это правило часто удивляет новичков: «Как же так, ведь фичи могут быть связаны?»

    Представьте: features/checkout импортирует features/apply-promo-code. Теперь изменение в apply-promo-code может сломать checkout. Вы получили скрытую связанность — именно то, от чего FSD защищает.

    Как решить проблему, если фичи действительно нужно взаимодействовать? Есть три пути:

    Путь 1: Общую логику опустить в entities или shared.

    Путь 2: Взаимодействие организовать через слой выше — widgets или pages.

    Путь 3: Использовать общий стор из entities как шину данных.

    Сегменты: стандартизация внутри слайса

    Внутри каждого слайса код делится на сегменты по технической роли. Стандартные сегменты:

    | Сегмент | Содержимое | Пример | |---|---|---| | ui | React-компоненты, стили | LoginForm.tsx, UserCard.tsx | | model | Стейт, хуки, бизнес-логика | useAuth.ts, userStore.ts | | api | Запросы к серверу | authApi.ts, userApi.ts | | lib | Утилиты, специфичные для слайса | formatUserName.ts | | config | Константы, конфигурация | authConfig.ts | | types | TypeScript-типы | types.ts |

    Не все сегменты обязательны. Маленькая фича может состоять только из ui и model. Главное — не изобретать свои названия без причины. Если вся команда использует model для стейта, а один разработчик создаёт store — это создаёт когнитивную нагрузку.

    Алиасы путей: обязательная настройка

    FSD без настроенных алиасов путей — это боль. Относительные пути вида ../../../../shared/ui/Button делают код нечитаемым и хрупким.

    Настройка алиасов в tsconfig.json:

    Настройка в vite.config.ts:

    После этого импорты становятся читаемыми и не зависят от глубины вложенности файла:

    Линтер для архитектурных правил

    Человеческий контроль за правилами зависимостей не работает в команде. Нужна автоматизация. Для этого существует плагин eslint-plugin-boundaries или специализированный @feature-sliced/eslint-config.

    Пример конфигурации с eslint-plugin-boundaries:

    Теперь попытка импортировать features из entities вызовет ошибку линтера прямо в редакторе — до того, как код попадёт в ревью.

    !Схема правил зависимостей FSD с допустимыми и запрещёнными направлениями импортов

    Типичные ловушки и как их избежать

    Ловушка 1: «Умные» страницы. Страница начинает содержать бизнес-логику — валидацию форм, расчёты, прямые вызовы API. Признак: в pages/checkout/ui/CheckoutPage.tsx больше 100 строк логики. Решение: вынести логику в model страницы или в фичу.

    Ловушка 2: Раздутый shared. Разработчики кладут в shared всё, что «может пригодиться». Через полгода там 300 файлов, половина из которых доменно-специфична. Признак: в shared есть useCurrentUser, cartHelpers, orderFormatter. Решение: регулярно аудировать shared и перемещать доменный код в entities.

    Ловушка 3: Слайсы-монолиты. Один слайс разрастается до нескольких тысяч строк. Признак: features/profile содержит 15 компонентов и 8 хуков. Решение: дробить по принципу «одна задача — один слайс».

    Ловушка 4: Игнорирование публичного API в shared. Разработчики думают, что правило публичного API касается только features и entities. Но shared тоже должен иметь index.ts для каждой подпапки:

    Когда FSD можно адаптировать

    FSD — это методология, а не жёсткий стандарт. Реальные команды адаптируют её под свои нужды. Как описывает команда flaton.systems, они убрали слой processes, добавили слой styles для глобальных стилей и сделали страницы основными архитектурными единицами.

    Это нормально. Главное — сохранять два фундаментальных принципа:

  • Однонаправленные зависимости: слои зависят только от нижних слоёв
  • Публичный API: слайсы общаются только через index.ts
  • Всё остальное — детали реализации, которые можно подстраивать под контекст проекта.

    5. Практическое применение: структура реального проекта

    Практическое применение: структура реального проекта

    Теория без практики — это просто красивые схемы. Давайте построим реальный проект с нуля: интернет-магазин с каталогом, корзиной, авторизацией и оформлением заказа. Пройдём весь путь — от анализа доменов до готовой структуры папок с реальным кодом.

    Шаг 1: Анализ доменов перед созданием папок

    Прежде чем открывать редактор, ответьте на три вопроса о вашем приложении. Это называют domain-first подходом, и он принципиально важен: структура папок должна отражать бизнес, а не технологии.

    Какие домены есть в приложении?

  • catalog — каталог товаров, категории, поиск
  • cart — корзина покупок
  • order — оформление и история заказов
  • user — профиль, авторизация
  • Какие сущности в каждом домене?

  • Product, Category — каталог
  • Cart, CartItem — корзина
  • Order, OrderItem — заказы
  • User, Address — пользователь
  • Какие пользовательские сценарии (фичи)?

  • auth-by-email, logout — авторизация
  • add-to-cart, remove-from-cart, update-cart-item — работа с корзиной
  • apply-promo-code — промокоды
  • checkout — оформление заказа
  • search-products, filter-products — поиск и фильтрация
  • edit-profile, change-password — профиль
  • Теперь у нас есть карта проекта. Можно строить структуру.

    Шаг 2: Полная структура проекта

    !Полная структура папок FSD-проекта интернет-магазина

    Шаг 3: Реализация ключевых частей

    Посмотрим, как конкретные части этой структуры реализуются в коде. Начнём с сущности cart — она центральная для нашего магазина.

    Теперь фича add-to-cart, которая использует этот стор:

    Шаг 4: Страница как финальная сборка

    Посмотрим, как страница каталога собирает всё вместе:

    Страница не знает, как работает фильтрация или поиск. Она только компонует готовые блоки.

    Шаг 5: Роутинг в App

    Как мигрировать существующий проект

    Если у вас уже есть проект без FSD, не нужно переписывать всё сразу. Команда pvsm.ru описывает реальный опыт: они внедряли FSD итерационно, раздел за разделом, и это заняло несколько спринтов.

    Практический план миграции:

  • Создайте папки слоёв рядом со старой структурой — app, pages, features, entities, shared
  • Настройте алиасы путей в tsconfig.json и vite.config.ts
  • Начните с shared — перенесите туда переиспользуемые UI-компоненты и утилиты
  • Выберите одну страницу и полностью перепишите её по FSD — это даст команде живой пример
  • При работе над новыми фичами сразу создавайте их в правильном слое
  • Постепенно переносите старый код при рефакторинге — не трогайте то, что работает и не меняется
  • Ключевое правило миграции: «локально до последнего». Если код используется только на одной странице — пусть живёт внутри этой страницы. Выносить в features или entities нужно только тогда, когда появляется второй потребитель.

    FSD — это не разовая задача, а способ мышления. Когда структура начинает отражать бизнес-домены, а не технические детали, проект становится понятным для всей команды — и для тех, кто его писал, и для тех, кто придёт позже.