Файловая структура проектов в компании
Рассказ о нашей архитектуре FBCA (Feature-Based Clean Architecture) — как устроена структура модулей, чем отличаются слои, слайсы и сегменты, куда класть код и зачем. Почему важно соблюдать модульность, как устроены зависимости и зачем нужны conventions. Полезно для новичков и опытных разработчиков.

Файловая структура WEB-продуктов YCLIENTS представляет из себя самостоятельно выработанное решение на базе FSD и называется FBCA
(Feature-Based Clean Architecture).
В этой статье ты сможешь узнать, что такое слой / слайс / сегмент, какими они бывают, и в какую же папку нужно положить свой код.
Слои
Верхнеуровнево модуль разделен на слои:
app
— инфраструктурный код для инициализации приложенияpages
— компоненты страниц (точки входа для роутера). Могут содержать бизнес-логику, но желательна декомпозиция в слои views / use-casesviews
— содержит компоненты и уникальную для них бизнес-логику, как самостоятельный сценарий, который может быть переиспользован в модуле — табы, модалки/диалоги, виджеты, обертки, функциональные области и т. д.use-cases
— общая переиспользуемая бизнес-логика между несколькими viewsshared
— содержит общие элементы оторванные от бизнес-логики
Связь между слоями организована по принципу луковичной структуры — модули на одном слое могут знать о модулях со слоев строго ниже. Сами связи между слоями выражается это в следующем виде: app → pages → views→ use-cases → shared
Слайсы
Каждый слой состоит из слайсов, а они в свою очередь из сегментов. При этом слои App и Shared, в отличие от других слоев, не имеют слайсов и состоят из сегментов напрямую.
Слайсы делят слой по предметной области. Вы можете называть ваши слайсы как угодно, и создавать их сколько угодно. Слайсы группируют тесно связанный по смыслу код. К примеру summary-card
, product-default
.
Сегменты
Слайсы, а также слои App
и Shared
, состоят из сегментов, которые группируют код по его назначению.
Всегда имеют плоскую структуру:
configs/
— конфигурации для модуляroutes/
— статичные массивы маршрутовplugins/
— плагины модуляclasses/
— реализации классов и (опционально) создание и ре-экспорт их глобальных инстансовconfigs/
— конфигурация модуляconstants/
— константы (объекты, массивы, примитивы)media/
— фото, гифки, видеоstyles/
— переменные, миксины, css-классыtypes/
— перечислители, типы, интерфейсыcomposables/
— композабл функцииcomponents/
— UI-компонентыstorages/
— работа сLS
,SS
,CS
,IndexedDb
store/
— модули стораworkers/
— логика создания. регистрации и коммуникации с воркером.api/
— работа с API: вызовы fetch и получение DTO бэкенда без их преобразований в модели компонентов.media/
— фото, гифки, видео не относящиеся к конкретному компоненту (например, иконка стрелки)styles/
— глобальные переменные, миксины и стили (например, reset стили)utils/
— недоменные утилиты (функции работы с внешними API, которые не содержат бизнес контекст). Также именно здесь содержится файлtracks.ts
, если он нужен, и реализует сбор продуктовой аналитики в рамках слайса
Верхнеуровневый пример файловой структуры
Общее описание слоев
marketplace/
├── app/
│ ├── configs/
│ ├── routes/
│ ├── plugins/
│ ├── router.ts
│ ├── store.ts
│ └── app.vue
├── pages/
│ └── catalog/
│ ├── classes/
│ ├── constants/
│ ├── media/
│ ├── styles/
│ ├── types/
│ ├── composables/
│ ├── utils/
│ ├── components/
│ ├── storages/
│ ├── store/
│ ├── workers/
│ ├── tracks/
│ └── index.ts
├── views/
│ └── catalog-filter/
│ ├── classes/
│ ├── constants/
│ ├── media/
│ ├── styles/
│ ├── types/
│ ├── composables/
│ ├── utils/
│ ├── components/
│ ├── storages/
│ ├── store/
│ ├── workers/
│ ├── tracks/
│ └── index.ts
│ └── product-default/
├── use-cases/
│ └── catalog-search/
│ ├── classes/
│ ├── constants/
│ ├── media/
│ ├── styles/
│ ├── types/
│ ├── composables/
│ ├── utils/
│ ├── storages/
│ ├── store/
│ ├── workers/
│ ├── tracks/
│ └── index.ts
│ └── index.ts
├── shared/
│ ├── api/
│ ├── media/
│ ├── styles/
│ └── utils/
app/
— инфраструктурный код для инициализации приложения
Содержит сегменты app/
слоя и доп. файлы:
configs/
routes/
plugins/
router.ts
store.ts
app.vue
pages/
— компоненты страниц (точки входа для роутера)
Компоненты страниц (точки входа для роутера). Могут содержать бизнес-логику, но желательна декомпозиция в слои views
/ use-cases
. index.ts
файл отдает только один компонент (точку входа). Также index.ts
может отдавать при необходимости props
и emits
.
Состоит из слайсов, которые делятся на сегменты:
classes/
constants/
media/
styles/
types/
composabled/
utils/
components/
storages/
store/
workers/
index.ts
— ре-экспорт ui-компонента страницы и типов для работы с ним
views/
— компоненты и уникальная для них бизнес-логику
Содержит компоненты и уникальную для них бизнес-логику, как самостоятельный сценарий, который может быть переиспользован в модуле — табы, модалки/диалоги, виджеты, обертки, функциональные области и т. д. Обязательно имеют ui-компонент. index.ts
файл отдает только один компонент и обязательно экспортирует props
и emits
этой точки входа.
Состоит из слайсов, которые делятся на сегменты:
classes/
constants/
media/
styles/
types/
composables/
utils/
components/
— ограничение на кол-во компонентов — 10 штstorages/
store/
workers/
index.ts
— ре-экспорты для ui-компонента и типов для работы с ним
use-cases/
— общая переиспользуемая бизнес-логика между несколькими views
Общая переиспользуемая бизнес-логика между несколькими views. Содержит только функциональную часть — ui полностью отсутствует. При необходимости поглащает потенциально объединяемые элементы. Состоит из слайсов и index.ts
файла, который аккумулирует в себе все структурные единицы из index.ts
файлов дочерних модулей.
Состоит из слайсов, которые делятся на сегменты:
classes/
constants/
media/
styles/
types/
models/
composables/
utils/
storages/
store/
workers/
index.ts
— ре-экспорты для фичи
shared/
— общие элементы оторванные от бизнес-логики
Содержит общие элементы оторванные от бизнес-логики. Функционал выносится сюда если используется более чем в 1 фиче и не содержит бизнес логику. Этот флоу работает только в одну сторону, к примеру — при удалении одной из двух фичей, которая использует этот функционал, обратно в фичу он уже не попадает. Считаем, что уже был
прецедент на переиспользование логики.
Состоит из сегментов:
api/
media/
styles/
utils/
Подход к написанию
Технически можно описать всю логику непосредственно в page
или views
. Однако такой подход может привести к нарушению принципа разделения ответственности и ухудшить масштабируемость. Строго рекомендуется выделять повторно используемые компоненты и логику в отдельные слои для лучшей модульности и поддержки архитектуры.
Вопросы и ответы
Чем слой shared отличается от use-cases? Зачем вообще нужен shared слой?
Разделение на слои use-cases
и shared
продиктовано необходимостью разграничения кода по его назначению и уровню привязки к бизнес-домену. use-cases
служит для объединения и переиспользования доменного функционала, который, несмотря на общность, остаётся привязанным к предметной области. shared
же хранит универсальные, инфраструктурные элементы, не связанные с бизнес-логикой, что позволяет сохранять чистоту и модульность архитектуры. К тому же, наличие shared явно показывает нам, что существуют зависимости от внешних библиотек, API, утилит или стилей.
Почему API в shared, если shared не должен содержать бизнес-логики?
Слой API представляет собой изолированный инфраструктурный уровень, который реализует универсальные механизмы связи с сервером и не содержит бизнес-правил или логики, характерных для конкретных доменов. Оно предоставляет лишь общие инструменты, которые могут быть использованы в любых фичах и в любых модулях.
Почему бы не иметь один общий слой «features» вместо views и use-cases?
Разделение на views
и use-cases
было произведено по нескольким ключевым причинам, связанным с соблюдением принципов DI, сохранением структурности и единообразия архитектуры, а также правильным распределением ответственности между слоями.
Рассмотрим их более детально:
1. Соблюдение принципов DI и инкапсуляции бизнес-логики:
- Мы разрешаем экспортировать из фичи только UI точку входа вместе с её пропсами и эмитами. Это помогает избежать неявного использования внутренних зависимостей и бизнес-логики напрямую, что соответствует принципам DI.
- Бизнес-логика, которая может использоваться несколькими фичами, выносится в отдельный слой (
use-cases
). Это обеспечивает строгую инкапсуляцию, так что UI-компоненты (изui
) взаимодействуют с бизнес-логикой через чётко определённые интерфейсы.
2. Сохранение структурности и единообразия:
- Если бы бизнес-логику вынесли в отдельную фичу, структура проекта стала бы неоднородной: в одних местах были бы фичи с UI, а в других — без него. Такое расхождение усложнило бы навигацию и понимание архитектуры.
3. Правильное распределение ответственности между слоями:
views
отвечает за визуальное представление и взаимодействие с пользователем, экспортируя только публичный интерфейс (UI точку входа).use-cases
содержит доменную (бизнес) логику, которая не должна смешиваться с представлением и при этом может использоваться несколькими фичами.shared
предназначен для универсальных утилит, зависимостей от внешних библиотек, стилей и прочего, что не относится к бизнес-логике, поэтому переносить туда специфичную доменную логику нельзя.
Почему внутри views можно экспортировать только один компонент и типы для работы с ним?
Такой подход побуждает писать самостоятельные и переиспользуемые компоненты. Когда каждая фича имеет единую публичную точку входа с четко определёнными типами, разработчики вынуждены думать о структуре и разделении ответственности, что помогает избежать превращения фичи в «помойку». Это способствует созданию модульных, тестируемых и легко поддерживаемых компонентов, которые можно повторно использовать в разных частях модуля.
Могу ли я весь код описать в page, а во views
/ use-cases
выносить только переиспользуемую логику?
Да, технически можно описать всю логику непосредственно в page. Однако такой подход может привести к нарушению принципа разделения ответственности и ухудшить масштабируемость. Строго рекомендуется выделять повторно используемые компоненты и логику в отдельные слои для лучшей модульности и поддержки архитектуры
Почему я могу брать всё содержимое из use-cases
, это же нарушает DI в том кейсе, что я знаю про устройство абстракции?
Если use-cases
экспортирует только четко определённые публичные интерфейсы и абстракции, которые инкапсулируют всю внутреннюю бизнес-логику, то использование этих интерфейсов не нарушает DI. То есть, ключевым моментом является то, как организован экспорт функциональности: доступ должен быть возможен только через публичный API, а не через прямой доступ ко всем внутренним деталям.
Почему не описана структура API и верхнеуровневая структура проекта?
Из-за большого объема данных в проектах верхнеуровневая структура и API-слой будут описаны в отдельных задачах. Такой подход позволит постепенно привести приложения к единообразию. Мы решили начать с низкоуровневых модулей, чтобы оставить пространство для экспериментов и доработок. По мере прояснения архитектурных аспектов мы сможем гибко адаптировать и расширять схему.
Обязательно ли иметь все указанные виды сегментов для слоя в page
/ views
/ use-cases
?
Нет, не обязательно. Эти сегменты являются рекомендациями для разделения ответственности и обеспечения масштабируемости. Если какая-либо функциональность не используется или не нужна для определённого слоя, можно опустить соответствующие сегменты, главное — сохранять единообразие и ясное разделение обязанностей там, где это имеет смысл.
Зачем нам иметь так много разных инфраструктурных сегментов (contstants
, storages
, types
…) не легче ли все объеденить в models
?
Если объединить всё в одну папку, получится «свалка», где трудно быстро найти нужное, а также нарушится принцип разделения ответственности. При явном разделении каждая категория кода становится более самостоятельной.
Куда размещать логику perfomance-логов, треков аналитики и прочих вспомогательных сервисов?
Доменные классы/функции, требуемые внутри конкретного доменного компонента/функции, размещаются сегментах семантически: если требуется класс — размещаем его в classes
, если функция — в utils
, если функция с реактивностью и/или методами жизненного цикла компонентов фрейвморка — composables
. При этом непосредственные запросы fetch и классы/функции для работы со вспомогательными сервисами должны располагаться в API-слое отдельно.