- исправлена генерация ссылок для architecture и examples - сохранена структура папок в архиве slm-design.zip - обновлён сгенерированный ARCHITECTURE.md
66 KiB
Локальная копия канонической спецификации SLM Design. Источник: https://slm-design.gromlab.ru/ARCHITECTURE.md Не редактировать вручную в этом проекте.
SLM Design
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
Разделы спецификации
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
- Слои — уровни организации
src/, направление зависимостей и зона ответственности каждого слоя. - Модули — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
- Сегменты — внутренние папки модуля (
ui/,parts/,hooks/,types/и другие) и правила размещения файлов. - Монорепозитории — применение SLM в
apps/иpackages/, правила выноса общих слоёв и ограничения для business.
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты → монорепозитории.
Преимущества
Вертикальная организация домена
Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы.
Dependency Injection без фреймворков
Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий.
Разделение ответственности без перегрузки слоёв
Сервисы приложения (infra/), UI-кит (ui/) и общие ресурсы (shared/) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода.
Горизонтальная инкапсуляция
Вложенные модули (parts/) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга.
Колокация по умолчанию
Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями.
Явное разделение каркаса и контента
Каркас группы маршрутов (layouts/) и контент конкретной страницы (screens/) — независимые слои с собственной ответственностью.
Масштабирование через группировку
При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции).
Адаптация к монорепозиториям
SLM применяется внутри каждого приложения, а packages/* используются только для общего кода из слоёв ui, infra и shared. Бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы.
Происхождение
SLM Design вырос на основе:
- Feature-Sliced Design — слоистая структура, публичный API модуля, направление зависимостей
- Vertical Slice Architecture — модуль как вертикальный срез, содержащий всё необходимое
- Screaming Architecture — структура проекта «кричит» о назначении: открыл
business/auth— видишь авторизацию - Colocation Principle — код живёт рядом с местом использования
Пример структуры проекта
src/
├── app/
│
├── layouts/
│ ├── main/
│ └── dashboard/
│
├── screens/
│ ├── home/
│ ├── products/
│ ├── product-detail/
│ └── about/
│
├── widgets/
│ ├── page-heading/
│ ├── hero-section/
│ └── promo-banner/
│
├── business/
│ ├── auth/
│ ├── catalog/
│ ├── orders/
│ └── chat/
│
├── infra/
│ ├── theme/
│ ├── i18n/
│ ├── backend-api/
│ └── logger/
│
├── ui/
│ ├── button/
│ ├── input/
│ ├── modal/
│ ├── toast/
│ └── dropdown/
│
└── shared/
├── lib/
├── types/
└── styles/
Принципы
- Домен — единое целое. Всё, что относится к домену, живёт в одном модуле.
- Колокация. Код рождается рядом с местом использования и поднимается только при необходимости.
- Зависимости однонаправлены. Импорты только сверху вниз, только через публичный API.
- Архитектура — каркас, не клетка. Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда.
Слои
Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом.
Определение
Слой — уровень организации кода внутри src/. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.
Группы слоёв
Слои делятся на три группы:
| Группа | Слои | Описание |
|---|---|---|
| Композиция | app, layouts, screens, widgets |
Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы |
| Ядро | business, infra, ui |
Реализация продукта: бизнес-домены, техсервисы, UI-кит |
| Фундамент | shared |
Общие ресурсы: утилиты, хелперы, стили, конфиги |
Направление зависимостей
Любой импорт между модулями — только через публичный API.
app → [ layouts | screens ] → widgets → business → infra → ui → shared
layoutsиscreens— параллельные слои, не импортируют друг друга- Модули одного слоя в группе «Композиция» изолированы друг от друга
- Модули одного слоя
infraиuiмогут импортировать друг друга через публичный API - Модули
business— cross-domain зависимости по коду через фабрику,import typeнапрямую - Импорт типов (
import type) в «Ядре» разрешён в обоих направлениях
Слой App
Точка входа приложения. Отвечает за запуск, роутинг и композицию маршрутов из layout и screen.
В отличие от остальных слоёв, app/ не содержит модулей SLM. Здесь живут только инфраструктурные файлы, которые не могут быть никаким другим слоем: файлы фреймворка роутинга, точка запуска и код инициализации.
Требования
- Не содержит модулей SLM — только файлы фреймворка, роутинг, инициализация
- Содержит: файлы маршрутов, bootstrap, обработку ошибок верхнего уровня (404, error boundary), подключение глобальных стилей и ассетов
- Провайдеры и гарды — только подключает готовые из нижних слоёв, не реализует
- Не содержит бизнес-логику, UI-компоненты, хуки, сторы, сервисы
- Никем не импортируется
Слой Layouts
Каркас страницы: общие элементы, одинаковые для группы маршрутов (header, footer, sidebar).
src/layouts/
├── main/
├── dashboard/
└── auth/
Требования
- Содержит только модули
- Не содержит бизнес-логику
- Контекстно-зависимые блоки принимает через пропсы от
app, не импортирует напрямую
Слой Screens
Контент конкретной страницы: собирает её из модулей нижних слоёв.
src/screens/
├── home/
├── products/
├── product-detail/
├── about/
└── contacts/
Когда количество страниц затрудняет навигацию — вводится группировка по разделам. Группа — папка для организации, не модуль (без index.ts).
src/screens/
├── shop/
│ ├── home/
│ ├── products/
│ ├── product-detail/
│ └── cart/
├── account/
│ ├── profile/
│ ├── settings/
│ └── order-history/
└── info/
├── about/
├── contacts/
└── faq/
Требования
- Содержит только модули
- Не содержит бизнес-логику
- Локальные одноразовые секции живут внутри screen-модуля, не выносятся в
widgets/business
Слой Widgets
Составной блок интерфейса, который компонует модули ядра, но не принадлежит конкретному бизнес-домену. Widget появляется когда блок используется в нескольких screens или layouts.
Если блок принадлежит домену — он живёт в business/{area}/, даже если переиспользуется. Если блок нужен только в одном месте — это screens/{name}/parts/ или layouts/{name}/parts/, а не widget.
src/widgets/
├── page-heading/
├── hero-section/
├── onboarding-checklist/
├── promo-banner/
└── error-boundary/
Требования
- Не принадлежит конкретному бизнес-домену. Если блок доменный — он живёт в
business/ - Используется в нескольких screens или layouts
Слой Business
Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, логикой, UI и сервисами.
Слой входит в группу «Ядро». Импортирует infra/, ui/, shared/. Каждый бизнес-модуль создаёт публичный runtime API через фабрику в корне. Cross-domain зависимости: runtime — через аргументы фабрики, типы — напрямую через import type.
Business объединяет то, что в FSD разделено на features и entities: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: types/ — доменная модель, hooks/ и services/ — сценарии и логика, mappers/ — трансформация данных, parts/ — составные блоки.
src/business/
├── auth/
├── catalog/
├── orders/
├── checkout/
└── chat/
Когда количество доменов затрудняет навигацию — вводится группировка по субдоменам. Группа — папка для организации, не модуль (без index.ts).
src/business/
├── commerce/
│ ├── catalog/
│ ├── cart/
│ ├── orders/
│ └── checkout/
└── communication/
├── chat/
└── notifications/
Требования
- Один модуль = один бизнес-домен
- Циклические зависимости между доменами запрещены
- Публичный runtime API — через фабрику в корне модуля (
{name}.factory.ts).index.tsэкспортирует только фабрику и type-only экспорты - Импорт runtime-кода между доменами — через фабрику.
import type— напрямую - Доменные типы (
User,Product) живут здесь, не вshared/
Слой infra
Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль.
Слой входит в группу «Ядро». Импортирует infra/, ui/, shared/.
Отличие от shared/: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), shared/ — общие ресурсы (утилиты, хелперы, стили, конфиги).
src/infra/
├── theme/
├── i18n/
├── backend-api/
├── maps-api/
├── logger/
├── feature-flags/
└── realtime/
Требования
- Один модуль = один техсервис
- Импортирует
infra/,ui/,shared/
Слой UI
UI-кит без бизнес-логики: button, carousel, toast, modal.
Слой входит в группу «Ядро». Импортирует ui/ и shared/.
Компоненты строятся друг на друге: button использует icon, carousel использует button.
src/ui/
├── button/
├── input/
├── icon/
├── carousel/
├── modal/
├── toast/
├── dropdown/
├── tabs/
└── tooltip/
Когда количество компонентов затрудняет навигацию — вводится группировка на примитивы и композиции. Примитивы (button, icon, input) не импортируют композиции. Композиции (carousel, modal, dropdown) строятся на примитивах.
src/ui/
├── primitives/
│ ├── button/
│ ├── input/
│ ├── icon/
│ └── badge/
└── composites/
├── carousel/
├── modal/
├── dropdown/
├── tabs/
└── tooltip/
Требования
- Не содержит бизнес-логику
- Импортирует только
ui/иshared/
Слой Shared
Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене.
Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует.
Отличие от infra/: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), shared/ — общие ресурсы (утилиты, хелперы, стили, конфиги).
Отличие от ui/: UI-компоненты (button, carousel, modal) живут в слое ui/, а не здесь.
src/shared/
├── lib/
├── types/
├── styles/
└── sprites/
Требования
- Не имеет runtime-состояния
Модули
Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом.
Определение
Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.
Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно.
Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность.
Главная граница модуля — не папка, а ответственность.
Компонент
Компонент — презентационная единица модуля, которая находится только в ui/ своего родительского модуля и отвечает за отображение части интерфейса.
Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля.
Компонент отображает. Модуль организует.
Компонент не может:
- Импортировать код проекта за пределами родительского модуля.
- Владеть архитектурными зависимостями.
- Содержать любые компоненты.
- Содержать любые модули.
- Делать внешние запросы.
- Самостоятельно получать данные.
- Выбирать источник данных.
- Композировать данные.
- Вызывать сценарные хуки.
- Оркестрировать сценарий.
- Композировать модули.
- Решать, как устроен процесс.
- Содержать бизнес-логику.
- Содержать сценарную логику.
Если компоненту требуется что-то из этого списка, он перестаёт быть компонентом и должен быть оформлен как модуль.
auth/
├── ui/
│ └── logout-button/
│ ├── logout-button.tsx
│ ├── styles/
│ │ └── logout-button.module.css
│ ├── types/
│ │ └── logout-button-props.type.ts
│ └── index.ts
└── index.ts
Что считается модулем
Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу.
Примеры модулей:
screens/home/— модуль страницы.widgets/page-heading/— модуль виджета.business/auth/— модуль бизнес-домена.infra/theme/— модуль инфраструктурного сервиса.ui/button/— модуль UI-kit сущности.screens/home/parts/hero-section/— вложенный модуль страницы.
Не считаются модулями:
ui/,parts/,hooks/,types/,styles/,config/— это сегменты.screens/shop/,business/commerce/— это группы, если в них нетindex.ts.screens/home/ui/user-card/— это компонент, если он находится вui/и соблюдает ограничения компонента.
Типы модулей
Тип модуля определяет обязательный корневой файл и стартовую структуру.
UI-модуль
Модуль строится вокруг основного UI-компонента и обязан иметь основной .tsx файл в корне:
header/
├── header.tsx
└── index.ts
ui/ внутри такого модуля используется только для компонентов, которые помогают корневому .tsx файлу.
Бизнес-модуль
Бизнес-модуль — модуль, который строится вокруг публичного runtime API.
Бизнес-модуль обязан иметь фабрику в корне:
auth/
├── auth.factory.ts
├── index.ts
└── types/
Фабрика возвращает публичный runtime API модуля.
Инфраструктурный модуль
Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции.
Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса.
theme/
├── index.ts
├── config/
├── hooks/
├── styles/
└── ui/
backend-api/
├── backend-api.client.ts
├── config/
├── types/
└── index.ts
Структура
Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности.
{module-name}/
├── {module-name}.factory.ts # фабрика (для business-модулей)
├── {module-name}.tsx # корневой файл модуля (опционален)
├── ui/ # компоненты модуля
├── parts/ # вложенные модули
├── hooks/ # хуки
├── stores/ # сторы состояния
├── services/ # внешние источники данных
├── mappers/ # трансформация данных между форматами
├── types/ # типы
├── styles/ # стили
├── lib/ # утилиты модуля
├── config/ # константы и конфигурация
└── index.ts # публичный API
Подробное описание сегментов — в разделе Сегменты.
Публичный API
Внешний код импортирует модуль только через публичный API.
// Хорошо
import { customerFactory } from '@/business/customer'
import type { Customer } from '@/business/customer'
// Плохо
import { validateToken } from '@/business/auth/lib/tokens'
index.ts модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи.
Внутренние сегменты модуля остаются деталями реализации.
Business-модуль экспортирует из index.ts только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из index.ts не экспортируются — они доступны через API, который возвращает фабрика.
// business/customer/index.ts
export { customerFactory } from './customer.factory'
export type { Customer } from './types/customer.type'
export type { CustomerApi } from './types/customer-api.type'
export type { CustomerDeps } from './types/customer-deps.type'
export type { CustomerFactory } from './types/customer-factory.type'
Фабрика
Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля ({name}.factory.ts), типизируется через {Name}Factory и возвращает публичный runtime API модуля.
Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика.
Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через export type — import type не является runtime-зависимостью.
Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция».
Примеры
Пример реализации фабрики в React см. в Создание фабрики.
Пример композиции фабрик в React screen-модуле см. в Композиция фабрик.
Жизненный цикл
Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности.
- Нужен на одной странице →
screens/{name}/parts/ - Появился в 2+ местах → поднимается по природе:
- абстрактный UI →
ui/ - блок с данными/логикой →
widgets/ - представление бизнес-домена →
business/{area}/parts/
- абстрактный UI →
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.
Сегменты
Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит.
Определение
Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.
Обзор
| Сегмент | Содержимое |
|---|---|
ui/ |
Презентационные компоненты родительского модуля |
parts/ |
Вложенные модули со своими сегментами |
hooks/ |
React-хуки |
stores/ |
Сторы состояния |
services/ |
Работа с внешними источниками данных |
mappers/ |
Трансформация данных между форматами |
types/ |
TypeScript-типы и интерфейсы |
styles/ |
Стили |
lib/ |
Утилиты и хелперы модуля |
config/ |
Константы и конфигурация |
Сегмент ui/
Презентационные компоненты родительского модуля. ui/ содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля.
Компонент в ui/:
- Находится в собственной папке.
- Может содержать только
{name}.tsx,index.ts,styles/,types/. - Не содержит любые компоненты.
- Не содержит любые модули.
- Не импортирует код проекта за пределами родительского модуля.
- Не делает внешние запросы.
- Не вызывает сценарные хуки.
- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные.
- Не содержит бизнес-логику или сценарную логику.
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе Компонент.
Корневой файл модуля в ui/ не размещается. Он лежит в корне модуля: {module-name}.tsx.
user/
├── ui/
│ ├── user-avatar/
│ │ ├── user-avatar.tsx
│ │ ├── styles/
│ │ │ └── user-avatar.module.css
│ │ ├── types/
│ │ │ └── user-avatar-props.type.ts
│ │ └── index.ts
│ └── user-status/
│ ├── user-status.tsx
│ └── index.ts
├── types/
├── hooks/
├── user.tsx
└── index.ts
Если UI-сущности нужна внутренняя декомпозиция, сценарная логика, получение данных или собственные архитектурные зависимости — это уже не компонент в ui/, а модуль в parts/.
Сегмент parts/
Вложенные модули со своими сегментами. parts/ содержит только модули: каждый элемент parts/ — папка полноценного модуля с собственным публичным API. Отдельные .tsx, стили, хуки или произвольные файлы в parts/ не размещаются.
home/
├── parts/
│ ├── hero-section/
│ │ ├── hero-section.tsx
│ │ ├── styles/
│ │ ├── parts/
│ │ │ └── top-banner/
│ │ │ ├── top-banner.tsx
│ │ │ └── index.ts
│ │ └── index.ts
│ └── features-section/
│ ├── features-section.tsx
│ ├── hooks/
│ └── index.ts
├── home.screen.tsx
└── index.ts
Отличие от ui/: элемент parts/ — модульная папка со своими сегментами. Элемент ui/ — компонент родительского модуля без собственной архитектурной ответственности.
Вложенность parts/ инкапсулирует область разработки горизонтально: каждый разработчик работает в своём parts/-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке.
Если вложенный модуль обрастает своими parts/ — это сигнал, что он достаточно самостоятельный для подъёма на уровень выше.
Сегмент hooks/
React-хуки модуля. Инкапсулируют логику, состояние, подписки, побочные эффекты.
hooks/
├── use-auth.hook.ts
├── use-session.hook.ts
└── use-permissions.hook.ts
Сегмент stores/
Сторы состояния модуля. Конкретная реализация зависит от выбранного стейт-менеджера (Zustand, MobX, Redux и т.д.).
stores/
├── auth.store.ts
└── session.store.ts
Сегмент services/
Работа с внешними источниками данных: API-вызовы, запросы, подписки.
services/
├── auth.service.ts
└── token.service.ts
Сегмент mappers/
Функции трансформации данных из одного формата в другой: DTO в доменный тип, доменный тип в DTO, доменный тип в ViewModel.
mappers/
├── map-user.ts
├── map-product.ts
└── map-order-to-dto.ts
Сегмент types/
TypeScript-типы и интерфейсы модуля. Доменные типы, DTO, пропсы компонентов.
types/
├── user.type.ts
└── session.type.ts
Сегмент styles/
Стили модуля. Формат зависит от выбранного подхода (CSS Modules, SCSS, CSS-in-JS и т.д.).
styles/
├── auth.module.css
└── login-form.module.css
Сегмент lib/
Утилиты и хелперы, специфичные для модуля. Чистые функции без побочных эффектов.
lib/
├── validate-email.ts
└── format-phone.ts
Отличие от shared/lib/: здесь лежат утилиты, нужные только этому модулю. Общие утилиты — в shared/lib/.
Сегмент config/
Константы и конфигурация модуля: маршруты, лимиты, дефолтные значения.
config/
├── routes.ts
└── constants.ts
Монорепозитории
Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в packages/ и какие ограничения действуют для общих пакетов.
Определение
Монорепозиторий — внешний уровень организации нескольких фронтенд-приложений и общих пакетов. SLM применяется внутри каждого приложения, а frontend-пакеты, относящиеся к SLM, содержат переиспользуемый код, вынесенный из слоёв ui, infra и shared.
Базовая структура
Каждое приложение внутри apps/ сохраняет собственную SLM-структуру в src/.
repo/
├── apps/
│ ├── web/
│ │ └── src/
│ │ ├── app/
│ │ ├── layouts/
│ │ ├── screens/
│ │ ├── widgets/
│ │ ├── business/
│ │ ├── infra/
│ │ ├── ui/
│ │ └── shared/
│ └── admin/
│ └── src/
│ └── ...
└── packages/
├── ui/
│ ├── button/ # самостоятельный пакет UI-модуля
│ ├── input/ # самостоятельный пакет UI-модуля
│ └── modal/ # самостоятельный пакет UI-модуля
├── infra/
│ ├── theme/ # самостоятельный пакет infra-модуля
│ ├── backend-api/ # самостоятельный пакет infra-модуля
│ └── logger/ # самостоятельный пакет infra-модуля
└── shared/ # единый shared-пакет
├── package.json
└── src/
├── lib/ # переиспользуемые утилиты
├── helpers/ # переиспользуемые helpers
└── index.ts
apps/{app}/src — граница SLM-приложения. packages/* находятся выше SLM и не добавляют новые архитектурные слои.
Группировка frontend-пакетов
Frontend-пакеты, вынесенные из SLM-приложений, рекомендуется группировать по источнику кода: ui, infra, shared.
packages/ui/* # пакеты UI-модулей
packages/infra/* # пакеты infra-модулей
packages/shared # единый shared-пакет
Эта группировка повторяет названия SLM-слоёв для навигации, но сама не является слоистой архитектурой внутри packages/. Монорепозиторий может содержать другие пакеты: tooling, конфиги, SDK, схемы, e2e и другие технические пакеты вне SLM.
Пакет и модуль
Пакет не равен SLM-модулю: модуль — архитектурная единица внутри слоя приложения, package — единица монорепозитория для переиспользования, владения, сборки и публикации.
В packages/ui/* размещаются пакеты самостоятельных UI-модулей. В packages/infra/* размещаются пакеты самостоятельных инфраструктурных модулей. packages/shared устроен иначе: это единый пакет для переиспользуемых утилит, helpers и другого фундаментального кода без привязки к конкретному приложению.
packages/ui/button/
packages/ui/modal/
packages/infra/theme/
packages/infra/backend-api/
packages/shared/
Что остаётся в приложении
Слои app, layouts, screens, widgets и business остаются внутри конкретного приложения.
apps/web/src/app/
apps/web/src/layouts/
apps/web/src/screens/
apps/web/src/widgets/
apps/web/src/business/
app, layouts и screens привязаны к роутингу, каркасу и страницам конкретного приложения. widgets не выносятся в пакеты, потому что это слой композиции интерфейса приложения.
business не выносится в packages/*. Домены остаются рядом со сценариями приложения, чтобы не превращать монорепозиторий в общий бизнес-слой.
Что можно выносить
В пакеты выносится только код из ui, infra и shared, который потенциально будет использоваться в двух и более фронтенд-приложениях монорепозитория.
| Группа | Что выносить | Пример |
|---|---|---|
packages/ui/* |
Самостоятельные UI-модули без бизнес-логики | packages/ui/button |
packages/infra/* |
Самостоятельные технические сервисы | packages/infra/backend-api |
packages/shared |
Общие утилиты, helpers и фундаментальный код | packages/shared |
Пакет можно создавать сразу, если модуль имеет общую природу и ожидается его переиспользование между приложениями. App-specific код остаётся внутри приложения.
UI-пакеты
В packages/ui/* размещаются переиспользуемые UI-модули.
packages/ui/button/
├── package.json
└── src/
├── button.tsx
├── styles/
├── types/
└── index.ts
UI-пакет не содержит бизнес-логику, обращения к API, сценарные хуки приложения и композицию страниц.
Infra-пакеты
В packages/infra/* размещаются переиспользуемые инфраструктурные модули.
packages/infra/backend-api/
├── package.json
└── src/
├── clients/
├── config/
├── types/
└── index.ts
Привязанные к конкретному приложению сервисы остаются в apps/{app}/src/infra. Например, локализация со словарями конкретного продукта остаётся в приложении; общим пакетом может быть только переиспользуемый i18n-движок.
Shared-пакет
packages/shared является единым пакетом.
packages/shared/
├── package.json
└── src/
├── lib/
├── helpers/
└── index.ts
В packages/shared сразу выносится общий фундаментальный код: чистые функции, helpers, утилиты, независимые константы и другой код без знания о продукте.
Проектные стили, типы приложения, продуктовые конфиги и ресурсы, завязанные на одно приложение, в общий shared не выносятся.
Имена пакетов и импорты
Путь импорта задаётся name в package.json, а не расположением директории.
{
"name": "@repo/theme"
}
packages/infra/theme/package.json
import { ThemeProvider } from '@repo/theme'
Пакеты должны импортироваться только через публичный API. Deep imports внутрь пакета запрещены.
// Хорошо
import { Button } from '@repo/button'
// Плохо
import { Button } from '@repo/button/src/button'
Зависимости
На уровне монорепозитория приложения зависят от пакетов, а пакеты не зависят от приложений.
apps → packages
packages -/→ apps
Внутри приложения продолжает действовать обычное направление зависимостей SLM.
app → [ layouts | screens ] → widgets → business → infra → ui → shared
Пакеты не должны нарушать природу своей группы: packages/ui/* не импортирует packages/infra/*, packages/shared не импортирует другие группы, а packages/infra/* не знает о приложениях.
Когда не выносить
Не выносите код в пакет, если он не может быть использован в двух и более фронтенд-приложениях, зависит от роутинга или страниц, содержит бизнес-логику, отражает продуктовую композицию конкретного интерфейса или не имеет стабильного публичного API.
Фактическое использование в одном приложении не запрещает пакет, если модуль имеет общую природу и потенциально нужен нескольким приложениям.
# Плохо
apps/web/src/screens/home/parts/promo-section/
packages/ui/promo-section/
Если блок нужен только одной странице или отражает продуктовую композицию конкретного приложения, он остаётся локальным parts/-модулем.
Конфигурационные пакеты
Конфигурационные пакеты не относятся к SLM-архитектуре.
Если в монорепозитории есть общие настройки TypeScript, ESLint, сборки или форматирования, они относятся к tooling-инфраструктуре репозитория. Такие пакеты могут находиться в packages/, но их структура зависит от выбранного инструментария и не участвует в правилах слоёв внутри src/.
Правила
- SLM применяется внутри каждого
apps/{app}/src. - Frontend-пакеты, вынесенные из SLM-приложений, группируются в
packages/ui,packages/infra,packages/shared. - Группы
packages/ui,packages/infra,packages/sharedне являются SLM-слоями. - В
packages/ui/*размещаются пакеты самостоятельных UI-модулей. - В
packages/infra/*размещаются пакеты самостоятельных инфраструктурных модулей. packages/sharedявляется единым пакетом для переиспользуемых утилит и helpers.- Модуль можно размещать в пакете, если он потенциально будет использоваться в двух и более фронтенд-приложениях.
business,app,layouts,screens,widgetsне выносятся в пакеты.- Проектные стили, типы приложения и продуктовые конфиги не выносятся в
packages/shared. - Пакеты не импортируют приложения.
- Межпакетные импорты идут только через публичный API.
- Deep imports внутрь пакетов запрещены.
- Локальная колокация важнее преждевременного выноса в
packages/*.
Создание фабрики
Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API.
Структура business-модуля
Фабрика лежит в корне business-модуля. Типы публичного API и зависимостей размещаются в types/.
business/customer/
├── customer.factory.ts
├── hooks/
├── types/
│ ├── customer.type.ts
│ ├── customer-api.type.ts
│ └── customer-factory.type.ts
├── ui/
└── index.ts
Тип публичного API
Публичный API описывает runtime-возможности, которые модуль отдаёт потребителям: хуки, компоненты и сценарные методы.
// business/customer/types/customer-api.type.ts
import type { ReactNode } from 'react'
import type { Customer } from './customer.type'
export type CustomerCardProps = {
customer: Customer
}
export type CustomerApi = {
useCustomer: () => Customer | null
CustomerCard: (props: CustomerCardProps) => ReactNode
}
// business/customer/types/customer-factory.type.ts
import type { CustomerApi } from './customer-api.type'
export type CustomerFactory = () => CustomerApi
Фабрика без зависимостей
Если модулю не нужны другие домены в runtime, фабрика создаётся без аргументов.
// business/customer/customer.factory.ts
import { useCustomer } from './hooks/use-customer.hook'
import { CustomerCard } from './ui/customer-card'
import type { CustomerFactory } from './types/customer-factory.type'
export const customerFactory: CustomerFactory = () => {
return {
useCustomer,
CustomerCard,
}
}
// business/customer/index.ts
export { customerFactory } from './customer.factory'
export type { Customer } from './types/customer.type'
export type { CustomerApi } from './types/customer-api.type'
export type { CustomerFactory } from './types/customer-factory.type'
Фабрика с зависимостями
Если модулю нужен другой домен в runtime, зависимость передаётся аргументом фабрики. Тип зависимости описывает только нужную часть API.
// business/order/types/order-deps.type.ts
import type { CustomerApi } from '@/business/customer'
export type OrderDeps = {
customer: Pick<CustomerApi, 'useCustomer'>
}
// business/order/types/order-factory.type.ts
import type { OrderApi } from './order-api.type'
import type { OrderDeps } from './order-deps.type'
export type OrderFactory = (deps: OrderDeps) => OrderApi
// business/order/order.factory.ts
import { createUseOrder } from './hooks/use-order.hook'
import { OrderCard } from './ui/order-card'
import type { OrderFactory } from './types/order-factory.type'
export const orderFactory: OrderFactory = (deps) => {
const useOrder = createUseOrder(deps)
return {
useOrder,
OrderCard,
}
}
Композиция фабрик
Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов.
Идея
Композиция фабрик выполняется в модуле-потребителе: screen, layout или другом модуле группы «Композиция». Business-модули не импортируют runtime-код друг друга напрямую, а cross-domain зависимости получают только через аргументы фабрик.
Структура screen-модуля
screens/home/
├── home.screen.tsx
└── index.ts
Сборка фабрик
Файл: screens/home/home.screen.tsx.
import { customerFactory } from '@/business/customer'
import { orderFactory } from '@/business/order'
const customer = customerFactory()
const order = orderFactory({ customer })
const { useOrder, OrderCard } = order
export const HomeScreen = () => {
const currentOrder = useOrder()
return <OrderCard order={currentOrder} />
}
customerFactory создаётся первой, потому что orderFactory зависит от части API домена customer. Модуль order не импортирует customer в runtime — зависимость передаётся снаружи.
Публичный API screen-модуля
Файл: screens/home/index.ts.
export { HomeScreen } from './home.screen'
Screen экспортирует только собственный публичный API. Собранные экземпляры business API остаются деталями реализации screen-модуля.
Композиция через Provider
Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя.
Идея
Screen получает готовый API бизнес-доменов через React Context. Граф фабрик собирается снаружи, например в роутере, а внутренние parts/ достают нужные домены через хук без пропс-дриллинга.
Принципы
- Принадлежность. Provider, Context и хук принадлежат конкретному screen-модулю и лежат в его сегментах.
- Внутренний тип. Тип композиции не экспортируется наружу — это закрывает доступ из бизнес-модулей.
- Внутренний хук. Хук доступа не экспортируется — доступен только внутри screen и его
parts/. - Публичный Provider. Только Provider экспортируется через
index.ts, чтобы роутер мог обернуть screen. - Сборка снаружи. Граф фабрик собирается в роутере или другом композиторе, screen фабрики не вызывает.
- Запрет для бизнеса. Бизнес-модули не используют провайдеры композиции. Cross-domain зависимости передаются только через аргументы фабрики.
Структура модуля
screens/main/
├── main.screen.tsx
├── providers/
│ └── main-composition.provider.tsx
├── hooks/
│ └── use-main-composition.hook.ts
├── types/
│ └── main-composition.type.ts
├── parts/
│ └── featured-products/
│ ├── featured-products.tsx
│ └── index.ts
└── index.ts
Сегмент providers/ — расширение стандартного набора SLM. Спецификация это разрешает: команда сама определяет, какие сегменты используются.
Распределение по сегментам
| Файл | Сегмент | Назначение |
|---|---|---|
main-composition.type.ts |
types/ |
TypeScript-тип композиции |
main-composition.provider.tsx |
providers/ |
Context и Provider-компонент |
use-main-composition.hook.ts |
hooks/ |
React-хук доступа |
main.screen.tsx |
корень | Корневой компонент screen-модуля |
featured-products/ |
parts/ |
Вложенный модуль со своим публичным API |
Тип композиции
Файл: screens/main/types/main-composition.type.ts.
Тип описывает, какие бизнес-домены доступны на этой странице. Он не экспортируется через index.ts, чтобы другие модули не зависели от внутренней формы композиции screen.
import type { CatalogApi } from '@/business/catalog'
import type { CartApi } from '@/business/cart'
export type MainComposition = {
catalog: CatalogApi
cart: CartApi
}
Context и Provider
Файл: screens/main/providers/main-composition.provider.tsx.
Context — внутренняя деталь Provider, наружу он не экспортируется. Значение null по умолчанию нужно, чтобы хук мог проверить отсутствие Provider в дереве.
Provider-компонент экспортируется через index.ts. Роутер передаёт в value уже собранный граф фабрик со стабильной ссылкой.
import { createContext, type ReactNode } from 'react'
import type { MainComposition } from '../types/main-composition.type'
export const MainCompositionContext = createContext<MainComposition | null>(null)
type Props = {
value: MainComposition
children: ReactNode
}
export const MainCompositionProvider = ({ value, children }: Props) => (
<MainCompositionContext.Provider value={value}>
{children}
</MainCompositionContext.Provider>
)
Хук доступа
Файл: screens/main/hooks/use-main-composition.hook.ts.
Хук остаётся внутренним и не экспортируется через index.ts модуля. Он доступен только внутри screen и его parts/.
Если хук используется вне Provider, он бросает ошибку. Это даёт раннюю диагностику неправильной композиции дерева.
import { useContext } from 'react'
import { MainCompositionContext } from '../providers/main-composition.provider'
export const useMainComposition = () => {
const ctx = useContext(MainCompositionContext)
if (!ctx) {
throw new Error('useMainComposition must be used within MainCompositionProvider')
}
return ctx
}
Сборка графа в роутере
Файл: app/router.tsx.
Роутер или другой композитор собирает граф фабрик в точке использования screen. Каждый домен получает свои зависимости через аргументы фабрики.
Фабрики вызываются вне React-компонента, если не зависят от runtime-параметров. Так API доменов не пересоздаётся на каждый рендер route-компонента.
import { MainScreen, MainCompositionProvider } from '@/screens/main'
import { catalogFactory } from '@/business/catalog'
import { cartFactory } from '@/business/cart'
import { authFactory } from '@/business/auth'
const auth = authFactory()
const catalog = catalogFactory()
const cart = cartFactory({ auth })
const MainRoute = () => (
<MainCompositionProvider value={{ catalog, cart }}>
<MainScreen />
</MainCompositionProvider>
)
Корневой компонент screen
Файл: screens/main/main.screen.tsx.
Screen получает нужные домены из композиции и достаёт из API готовые хуки, компоненты или функции. В JSX используются уже локальные useCategories и CategoryList, а не обращение к фабричному API через точку.
import { useMainComposition } from './hooks/use-main-composition.hook'
import { FeaturedProducts } from './parts/featured-products'
export const MainScreen = () => {
const { catalog } = useMainComposition()
const { useCategories, CategoryList } = catalog
const categories = useCategories()
return (
<div>
<CategoryList categories={categories} />
<FeaturedProducts />
</div>
)
}
Вложенный part
Файл: screens/main/parts/featured-products/featured-products.tsx.
Вложенный модуль получает доступ к той же композиции родительского screen. Промежуточные компоненты не прокидывают домены через props.
Из API доменов достаются готовые сущности: useFeatured, ProductCard и addItem. Компонент работает с ними напрямую.
import { useMainComposition } from '../../hooks/use-main-composition.hook'
export const FeaturedProducts = () => {
const { catalog, cart } = useMainComposition()
const { useFeatured, ProductCard } = catalog
const { addItem } = cart
const products = useFeatured()
return (
<div>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
onAdd={() => addItem(product.id)}
/>
))}
</div>
)
}
Файл: screens/main/parts/featured-products/index.ts.
export { FeaturedProducts } from './featured-products'
Публичный API screen-модуля
Файл: screens/main/index.ts.
Наружу экспортируются только screen и его Provider. MainComposition, MainCompositionContext и useMainComposition остаются деталями реализации.
export { MainScreen } from './main.screen'
export { MainCompositionProvider } from './providers/main-composition.provider'
Почему тип композиции не экспортируется
Внутренний тип закрывает доступ к форме композиции из внешних модулей. Бизнес-модуль не должен знать, какие домены собраны для конкретного screen.
Такой импорт из бизнес-модуля не должен быть возможен через публичный API screen.
import type { MainComposition } from '@/screens/main'
Когда тип остаётся внутренним, такая связь невозможна через публичный API screen-модуля.
Почему хук не экспортируется
Если хук доступа сделать публичным, любой модуль сможет вызвать его напрямую. Внутренний хук доступен только через относительные импорты внутри screen-модуля и его parts/.
Почему Provider экспортируется
Provider безопасно экспортировать: сам по себе он не даёт доступ к данным, а только принимает готовую композицию и передаёт её детям внутри React-дерева.
Стабильность value
Фабрики создаются на уровне модуля, поэтому catalog и cart сохраняют ссылки между рендерами MainRoute.
Если домены зависят от runtime-параметров, граф нужно собирать в отдельном композиторе для этих параметров и передавать в Provider уже готовую композицию.
Расширение на другие screen-модули
Паттерн повторяется для каждого screen, которому нужна композиция бизнес-доменов.
screens/checkout/providers/checkout-composition.provider.tsx
screens/checkout/hooks/use-checkout-composition.hook.ts
screens/checkout/types/checkout-composition.type.ts
Имена включают имя screen-модуля. Не используйте универсальные названия вроде useComposition или useScope: по имени файла должно быть понятно, к какой странице привязан Context.