Был FSD — стал MSD: как мы допилили методологию FSD, чтобы поудобнее делить монолит на модули

Публикации в СМИ
27.10.2025
Опубликовано на
Хабре
287c3d7f6cc23647c709d89c36b70d12.jpeg

Что важно фронтенд-разработчику при создании веб-приложений? Поддержка текущей кодовой базы, удобство внедрения новых фич и возможность повторно использовать компоненты. Создать такие условия помогает популярный подход к проектированию — FSD (Feature Sliced Design). Разбиваем интерфейс на независимые, переиспользуемые модули (виджеты, фичи и т. д.), получаем чёткие правила, единую структуру проекта и ускорение разработки за счёт переиспользования кода и изоляции ответственности.

Подход FSD во многом прекрасен, но всё же нам в нём не хватало некоторых важных аспектов: внятного разделения слоёв бизнес-логики, удобства работы с кастомными хуками (они быстро разрастаются, обрастают связями и становятся сложными для тестирования). Также было неясно, куда выносить сложные общие компоненты из разных частей проекта. И, например, как легко отделять один бизнес-модуль от другого, не ломая всю систему…Меня зовут Иван Соснович, я тимлид фронтенд-разработки в СберТехе, тружусь в команде Platform V Kintsugi — это графический инструмент для сопровождения, мониторинга и диагностики Postgres-like СУБД. В этой статье я покажу, как мы доработали FSD под себя, и дам ссылку на пример со структурой приложения. Надеюсь, будет полезно фронтенд-разработчикам.Методология FSD позволяет организовать структурированный подход к разработке ПО. Что можно сделать с её помощью?

  1. Сделать архитектуру понятнее. Код разбивают на независимые модули, что обеспечивает логичную структуру и удобство навигации по проекту. Например, модули авторизации, профиля и других функций отделены друг от друга, что значительно улучшает читаемость и понимание кода.
  2. Повысить поддерживаемость кода. Каждый модуль ограничен своей зоной ответственности. Проще вносить изменения и исправления. Работа над отдельной функциональностью не нарушает работу остальных частей системы.
  3. Можно переиспользовать код. Модули и логика могут свободно использоваться в различных частях приложения без дублирования, что снижает затраты ресурсов и повышает эффективность разработки.
  4. Улучшить масштабируемость. Новые фичи легко интегрируются в систему как отдельные модули, не нарушая существующую архитектуру.
  5. Тестировать становится удобнее. Чётко очерченные границы модулей облегчают написание и поддержку тестов.

Слои в рамках подхода FSD

1. App.

  1. Назначение: инициализация приложения.
  2. Содержимое: включает глобальные настройки (например, темы), роутинг и провайдеры контекста.
  3. Примеры файлов: App.tsxAppRouter.tsx.

2. Entities.

  1. Назначение: хранение бизнес-сущностей и основной логики работы приложения.
  2. Содержимое: содержат определения сущностей (например, UserProduct) и бизнес-логику, связанную с ними.
  3. Примеры файлов: entities/Userentities/Product.

3. Features.

  1. Назначение: реализация конкретных пользовательских действий.
  2. Содержимое: компоненты, хуки и логика, которая обеспечивает выполнение задач пользователями (авторизация, добавление товаров в корзину и др.).
  3. Примеры файлов: features/Loginfeatures/AddToCart.

4. Shared.

  1. Назначение: общие утилиты, типы и компоненты, используемые в разных частях приложения.
  2. Содержимое: переиспользуемые компоненты (например, кнопки), утилиты и глобальные типы.
  3. Примеры файлов: shared/Buttonshared/hooksshared/utils.

5. Pages.

  1. Назначение: сборка всех компонентов для формирования страниц приложения.
  2. Содержимое: страницы, использующие компоненты из слоёв FeaturesEntities и Shared.
  3. Примеры файлов: pages/HomePagepages/ProductPage.

6. Widgets.

  1. Назначение: повторяющиеся крупные блоки интерфейса, которые можно использовать многократно.
  2. Содержимое: логика и UI-компоненты (например, новости, карусели).
  3. Примеры файлов: widgets/NewsCarouselwidgets/UserProfile.

7. Processes (опционально).

  1. Назначение: вынос сложных процессов, объединяющих несколько функциональных возможностей.
  2. Содержимое: бизнес-процессы, такие как оформление заказов.
  3. Примеры файлов: processes/Checkout.

Всё хорошо, но… Чего нам не хватало в FSD?

Несмотря на очевидные преимущества, в базовом подходе FSD нам не хватало некоторых важных аспектов:

  • Грамотно расписанных слоёв бизнес-логики.
  • Гибкости модульности (именно на уровне бизнес-логики).
  • Кастомные хуки разрастались внутри, обрастали множественными связями, в результате их становилось трудно тестировать.
  • Не было чёткого понимания, где размещать общие сложные компоненты, используемые в нескольких частях проекта.
  • И нельзя было легко отделять один бизнес-модуль от другого без нарушения общей функциональности. 

Мы решили кастомизировать…

Знакомьтесь, MSD 

… и получился свой подход, который назвали MSD (Modules Sliced Design). Буквально можно перевести как «проектирование на основе модульных слайсов (срезов)». Он основан на принципах FSD, но дополнен нашими идеями и решениями:

Основные принципы

Каждый слой (Pages, Widgets, features) имеет одинаковую семантику папок. Слой pages:

├── pages/ │ ├── user/ │ │ ├── create/ │ │ │ ├── ui/ │ │ │ │ ├── index.jsx │ │ │ │ ├── index.styled.ts │ │ │ │ ├── index.types.ts │ │ │ ├── index.js export { Create as CreateUserPage } from './ui' — заменяем имя страницы при экспорте │ │ ├── edit/ │ │ │ ├── ui/ │ │ │ │ ├── index.jsx │ │ │ │ ├── index.styled.ts │ │ │ │ ├── index.types.ts │ │ │ ├── index.js │ │ ├── settings/ │ │ │ ├── ui/ │ │ │ │ ├── index.jsx │ │ │ │ ├── index.styled.ts │ │ │ │ ├── index.types.ts │ │ │ ├── index.js │ │ ├── index.js export * from './create' — отдаём во внешний мир всё, что разрешает сама страница src/ │ ├── pages/ │ ├── user/ │ │ ├── create/ │ │ │ ├── ui/ │ │ │ │ ├── index.jsx │ │ │ │ ├── index.styled.ts │ │ │ │ ├── index.types.ts │ │ │ ├── index.js export { Create as CreateUserPage } from './ui' — заменяем имя страницы при экспорте │ │ ├── edit/ │ │ │ ├── ui/ │ │ │ │ ├── index.jsx │ │ │ │ ├── index.styled.ts │ │ │ │ ├── index.types.ts │ │ │ ├── index.js │ │ ├── settings/ │ │ │ ├── ui/ │ │ │ │ ├── index.jsx │ │ │ │ ├── index.styled.ts │ │ │ │ ├── index.types.ts │ │ │ ├── index.js │ │ ├── index.js export * from './create' — отдаём во внешний мир всё, что разрешает сама страница │ ├── widgets/ │ ├── user/ │ │ ├── create/ │ │ │ ├── form/ │ │ │ │ ├── config/ │ │ │ │ ├── constants/ │ │ │ │ ├── lib/ │ │ │ │ ├── ui/ — внутри лежат все компоненты для реализации этого функционала │ │ │ ├── index.js │ │ │ ├── header/ │ │ │ │ ├── config/ │ │ │ │ ├── constants/ │ │ │ │ ├── lib/ │ │ │ │ ├── ui/ │ │ │ │ ├── index.js │ │ │ ├── footer/ │ │ │ │ ├── config/ │ │ │ │ ├── constants/ │ │ │ │ ├── lib/ │ │ │ │ ├── ui/ │ │ │ │ ├── index.js │ │ │ ├── index.js export * from './create' — отдаём во внешний мир всё, что разрешает сам виджет │ │ ├── edit/... │ │ ├── settings/... │ │ ├── index.js export * from './user' — отдаём во внешний мир всё, что разрешают сами виджеты | ├── features/ │ ├── user/ │ │ ├── create/ │ │ │ ├── form/ │ │ │ │ ├── config/ │ │ │ │ ├── constants/ │ │ │ │ ├── lib/ │ │ │ │ ├── ui/ внутри лежат все компоненты для реализации этого функционала │ │ │ ├── index.js │ │ │ ├── header/ │ │ │ │ ├── config/ │ │ │ │ ├── constants/ │ │ │ │ ├── lib/ │ │ │ │ ├── ui/ │ │ │ │ ├── index.js │ │ │ ├── footer/ │ │ │ │ ├── config/ │ │ │ │ ├── constants/ │ │ │ │ ├── lib/ │ │ │ │ ├── ui/ │ │ │ │ ├── index.js │ │ │ ├── index.js export * from './create' — отдаём во внешний мир всё, что разрешает сама фича │ │ ├── edit/... │ │ ├── settings/... │ │ ├── index.js export * from './user' — отдаём во внешний мир всё, что разрешает сам набор фичей.

Слой shared содержит функционал, не привязанный к бизнес-логике, и он должен переноситься в другой проект без каких-либо манипуляций.

├── ui/ — и все простые компоненты проекта не привязаны ни к какой логике проекта ├── api/ — базовая настройка слоя взаимодействия (например, настройка axios) ├── theme/ — провайдер темы библиотеки, без которых не могут существовать ui-компоненты ├── hooks/ — общие хуки приложения (useDebounse, useOutsideClick), именно те хуки, которые могут быть переиспользованы в других проектах ├── lib/ — утилиты (например, копирование значения в буфер, работа с local storage) ├── "@types"/ — декораторы типов

Слой app — для настройки всего проекта. По сути, от FSD отличий нет, кроме того, что убрана тема.

Слой entities — тоже без изменений по сравнению с FSD, но теперь тут нет UI, порядок вложения как и у слоёв Pages:

├── pages/ │ ├── user/ │ │ ├── create/ │ │ ├── DTO/ — все типы для взаимодействия с бэкэндом │ │ ├── types/ — все типы для внутреннего использования │ │ ├── lib/ — утилиты, которые требуется использовать внутри данной сущности │ │ ├── api/ — все необходимые вызовы API для данной сущности │ │ ├── store/ стор — по сущности, далее по нему будет подробный раздел │ │ ├── parsers?/ Не все готовы перебирать из типов DTO в типы для использования проекта, так как может появиться множество дублей. Мы решили пока отказаться от этого. │ │ │ ├── index.js │ │ ├── edit/... │ │ │ ├── index.js │ │ ├── settings/... │ │ │ ├── index.js │ │ ├── index.js

 И появляется новое — слой composition/ Зачем? Для чего? 

├── ui/ — сложные компоненты, которые могут быть переиспользованы в разных местах, но везде должны быть привязаны к одному типу из entities ├── layer/ — слои для формирования расположения компонентов ├── settings/ — настройки проекта, общие для всех. Таймеры, фича-тоглы и так далее. ├── components/ - сложные компоненты проекта для переиспользования (formField, widgets) ├── hooks/ - хуки привязанные к логике проекта, для переиспользования в разных частях

Что у нас получилось по слоям и их функциональности:

1. App.

  1. Назначение: слой для инициализации приложения.
  2. Содержит: роутинг, провайдеры контекста/store, хуки настроек, хуки первого рендера и так далее.

2.Entities.

  1. Назначение: здесь хранятся бизнес-сущности — основные модели и их логика, без UI сущностей.
  2. Содержит: определения сущностей имеет модульный подход для быстрого отделения их в другой проект.

3. Features.

  1. Назначение: модули, которые реализуют конкретные пользовательские действия.
  2. Содержит: только UI-сущности, которые сами решают, в каком виде появиться. Объединяют в себе компоненты из слоёв shared и composition, а также логику из слоя entities.

4. Shared.

  1. Назначение: общий слой, содержит тему проекта и то, что может быть использовано в другом проекте.
  2. Содержит: переиспользуемые компоненты (например, кнопки), утилиты, глобальные типы.

5.Pages.

  1. Назначение: собирает все компоненты, чтобы сформировать страницы приложения.
  2. Содержит: страницы, которые используют только widgets-компоненты. С редким исключением — логику из Entities.

6.Widgets.

  1. Назначение: крупные, повторяющиеся блоки, которые можно переиспользовать на разных страницах или только на одной.
  2. Содержит: модули с логикой и UI (например, блоки новостей, карусели).

7. Composition. Назначение: общий слой всего проекта, для возможного использования во всех слоях

  1. settings — глобальные настройки приложения;
  2. components — UI-компоненты для общего использования.

Выбор менеджера состояний

Да, для управления состоянием приложения мы выбрали библиотеку Zustand. Её преимущества: изоляция состояний, простота интеграции с компонентами, высокая производительность и лёгкость тестирования, минимум внешних зависимостей и возможность вызова одного экшена внутри другого.

Результат

Собрали обратную связь у разработчиков: говорят, что с MSD стало проще тестировать код. При доработке функциональности проще дополнять чем-то новым и тестировать реализации. А при работе в большой команде меньше конфликтов в pull request'ах.В общем, мы довольны нашими преобразованиями. И есть планы на будущее. Например, хотим реализовать расширения на VsCode, чтобы быстрее и удобнее работать со структурой. Было бы интересно детальнее разобрать каждый слой с учётом потребностей нескольких приложений. И ещё проверить гипотезу простого разделения на микросервисы.

Выложил здесь пример структуры приложения. Там структура проекта, распределение по слоям, а также связи между слоями и их содержание. Буду рад, если пригодится. А здесь наше сообщество, где мы время от времени выкладываем вакансии и пишем про разработку и всё, что с ней связано. 

Спасибо за внимание!