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

Подход FSD во многом прекрасен, но всё же нам в нём не хватало некоторых важных аспектов: внятного разделения слоёв бизнес-логики, удобства работы с кастомными хуками (они быстро разрастаются, обрастают связями и становятся сложными для тестирования). Также было неясно, куда выносить сложные общие компоненты из разных частей проекта. И, например, как легко отделять один бизнес-модуль от другого, не ломая всю систему…

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

Методология FSD позволяет организовать структурированный подход к разработке ПО. Что можно сделать с её помощью?

  1. Сделать архитектуру понятнее. Код разбивают на независимые модули, что обеспечивает логичную структуру и удобство навигации по проекту. Например, модули авторизации, профиля и других функций отделены друг от друга, что значительно улучшает читаемость и понимание кода.

  2. Повысить поддерживаемость кода. Каждый модуль ограничен своей зоной ответственности. Проще вносить изменения и исправления. Работа над отдельной функциональностью не нарушает работу остальных частей системы.

  3. Можно переиспользовать код. Модули и логика могут свободно использоваться в различных частях приложения без дублирования, что снижает затраты ресурсов и повышает эффективность разработки.

  4. Улучшить масштабируемость. Новые фичи легко интегрируются в систему как отдельные модули, не нарушая существующую архитектуру.

  5. Тестировать становится удобнее. Чётко очерченные границы модулей облегчают написание и поддержку тестов.

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

  1. App.

    1. Назначение: инициализация приложения.

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

    3. Примеры файлов: App.tsx, AppRouter.tsx.

  2. Entities.

    1. Назначение: хранение бизнес-сущностей и основной логики работы приложения.

    2. Содержимое: содержат определения сущностей (например, User, Product) и бизнес-логику, связанную с ними.

    3. Примеры файлов: entities/User, entities/Product.

  3. Features.

    1. Назначение: реализация конкретных пользовательских действий.

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

    3. Примеры файлов: features/Login, features/AddToCart.

  4. Shared.

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

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

    3. Примеры файлов: shared/Button, shared/hooks, shared/utils.

  5. Pages.

    1. Назначение: сборка всех компонентов для формирования страниц приложения.

    2. Содержимое: страницы, использующие компоненты из слоёв Features, Entities и Shared.

    3. Примеры файлов: pages/HomePage, pages/ProductPage.

  6. Widgets.

    1. Назначение: повторяющиеся крупные блоки интерфейса, которые можно использовать многократно.

    2. Содержимое: логика и UI-компоненты (например, новости, карусели).

    3. Примеры файлов: widgets/NewsCarousel, widgets/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. Назначение: общий слой всего проекта, для возможного использования во всех слоях:

      1. settings — глобальные настройки приложения;

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

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

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

Результат

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

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

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

Спасибо за внимание! Если есть вопросы, предложения, идеи, приглашаю писать в комментарии.

Комментарии (2)


  1. nin-jin
    27.10.2025 07:37

    NeuroSlopDesign


  1. akardapolov
    27.10.2025 07:37

    библиотеку Zustand

    А почему не mobx?