Олег Марков
Архитектура FSD для Angular - практическое руководство
Введение
Подход Feature Sliced Design (FSD) появился как ответ на проблему роста сложности фронтенд‑приложений. Чем больше экранов и бизнес‑логики, тем тяжелее поддерживать код, в котором всё «перемешано» по техническим признакам — по видам слоев (components, services, directives и т.п.) или по модулям, которые отражают только маршруты.
В экосистеме React FSD уже достаточно распространен. В Angular мире подход только набирает обороты, и часто его называют angular-fsd — просто как адаптацию идей FSD под стандартные инструменты Angular (NgModules, standalone components, DI, RxJS).
Здесь я покажу вам, как можно:
- переложить идеи FSD на Angular;
- построить структуру проекта;
- организовать зависимости между слоями;
- использовать Angular‑специфику (модули, DI, lazy loading) так, чтобы она не ломала архитектуру;
- постепенно мигрировать к FSD в уже существующем проекте.
Я буду опираться на классическую концепцию FSD: слои (layers), срезы по фичам (features), ограничение зависимостей и явные публичные API.
Базовые принципы FSD в Angular
Что такое FSD в контексте Angular
Идея FSD — строить архитектуру вокруг бизнес‑возможностей (фич), а не вокруг технических сущностей. Вместо структуры вида:
- components/
- services/
- models/
- pages/
вы двигаетесь к структуре:
- app/
- processes/
- pages/
- features/
- entities/
- shared/
где каждый слой решает свою задачу и имеет четкие правила, кому он может зависеть и что может экспортировать.
В Angular эта концепция хорошо ложится на:
- модули (NgModule / standalone component + routing);
- сервисы (через DI);
- файлы с моделями и утилитами (TypeScript);
- lazy loading модулей по маршрутам.
Смотрите, концептуально FSD для Angular отвечает на вопросы:
- Куда положить новый компонент?
- Где разместить бизнес‑логику?
- Какие зависимости допустимы между частями приложения?
- Как сделать так, чтобы код не превращался в «общую свалку» shared?
Основные слои в angular-fsd
На практике чаще всего используют 5–6 слоев:
- app — точка входа, глобальная конфигурация приложения.
- processes — долгоживущие пользовательские сценарии, которые объединяют несколько страниц и фич.
- pages — экраны и маршруты (то, что привязано к URL).
- features — независимые бизнес‑функции, которые могут быть использованы на разных страницах.
- entities — бизнес‑сущности (User, Product, Order и т.п.).
- shared — переиспользуемые «технические» части: UI‑компоненты, утилиты, библиотеки.
Чуть дальше я покажу, как это выглядит в структуре файлов и модулей.
Структура проекта angular-fsd
Базовый шаблон структуры
Давайте разберемся на примере условного интернет‑магазина:
- каталог товаров;
- корзина;
- оформление заказа;
- профиль пользователя.
Предложенная структура:
src/
app/
app.module.ts
app-routing.module.ts
providers/ // Глобальные провайдеры, если нужны
layout/ // Общий каркас приложения (shell)
processes/
checkout/ // Цепочка корзина -> оформление -> оплата
pages/
home/
catalog/
product/
cart/
profile/
features/
add-to-cart/
manage-cart/
auth-by-email/
update-profile/
entities/
product/
cart/
user/
shared/
ui/
button/
input/
modal/
lib/
form/
date/
http/
config/
styles/
Ключевые моменты:
- каждая директория верхнего уровня — это слой;
- внутри слоев — папки конкретных фич, сущностей или страниц;
- у каждой фичи/сущности есть собственное «локальное» API.
Пример структуры одной фичи
Теперь вы увидите, как это выглядит внутри фичи add-to-cart:
features/
add-to-cart/
ui/ // Компоненты UI этой фичи
add-to-cart-button/
model/ // Бизнес-логика, состояние и эффекты
add-to-cart.facade.ts
add-to-cart.types.ts
lib/ // Вспомогательные функции
map-product-to-cart-item.ts
index.ts // Публичный API фичи
Обратите внимание:
- UI и логика разнесены, но остаются «рядом» внутри фичи;
- наружу вы экспортируете только то, что нужно — через
index.ts; - извне никто не лезет во внутреннюю структуру фичи напрямую.
Пример index.ts:
// features/add-to-cart/index.ts
// Экспортируем публичные компоненты и сервисы фичи
export * from './ui/add-to-cart-button/add-to-cart-button.component';
export * from './model/add-to-cart.facade';
export * from './model/add-to-cart.types';
Так вы формируете четкий контракт фичи: все, что нужно внешнему коду, импортируется только из features/add-to-cart.
Слои и их ответственность
Слой app
Слой app отвечает за:
- инициализацию приложения;
- глобальный layout (header, sidebar);
- глобальный роутинг;
- регистрацию глобальных провайдеров.
Структурно:
app/
app.module.ts
app-routing.module.ts
layout/
shell/
shell.component.ts
Пример app-routing.module.ts с lazy loading страниц:
// app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ShellComponent } from './layout/shell/shell.component';
const routes: Routes = [
{
path: '',
component: ShellComponent, // Общий макет приложения
children: [
{
path: '',
loadChildren: () =>
import('../pages/home/home.module').then(m => m.HomeModule),
},
{
path: 'catalog',
loadChildren: () =>
import('../pages/catalog/catalog.module').then(m => m.CatalogModule),
},
{
path: 'cart',
loadChildren: () =>
import('../pages/cart/cart.module').then(m => m.CartModule),
},
],
},
];
@NgModule({
imports: [
// Здесь мы регистрируем маршруты приложения
RouterModule.forRoot(routes),
],
exports: [RouterModule],
})
export class AppRoutingModule {}
Комментарии:
- здесь мы не пишем бизнес‑логику;
- роутинг перенаправляет на модули страниц (
pages).
Слой pages
pages — это экраны. Они:
- определяют маршруты (route config) для конкретного сегмента URL;
- собирают из фич и сущностей интерфейс страницы;
- не содержат «глубокую» бизнес‑логику.
Пример страницы CatalogPage:
pages/
catalog/
catalog.module.ts
catalog-routing.module.ts
ui/
catalog-page/
catalog-page.component.ts
Теперь давайте посмотрим, что происходит в модуле:
// pages/catalog/catalog.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CatalogRoutingModule } from './catalog-routing.module';
import { CatalogPageComponent } from './ui/catalog-page/catalog-page.component';
import { ProductListComponent } from '../../entities/product/ui/product-list/product-list.component';
import { AddToCartButtonComponent } from '../../features/add-to-cart';
@NgModule({
declarations: [
CatalogPageComponent, // Страница как контейнер
],
imports: [
CommonModule,
CatalogRoutingModule,
ProductListComponent, // Standalone компонент сущности
AddToCartButtonComponent, // Standalone компонент фичи
],
})
export class CatalogModule {}
А вот пример компонента страницы:
// pages/catalog/ui/catalog-page/catalog-page.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ProductListFacade } from '../../../../entities/product/model/product-list.facade';
@Component({
selector: 'app-catalog-page',
templateUrl: './catalog-page.component.html',
styleUrls: ['./catalog-page.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CatalogPageComponent {
// Здесь мы получаем данные каталога из сущности Product
products$ = this.productListFacade.products$;
constructor(
// Внедряем фасад, который инкапсулирует работу с сущностью Product
private readonly productListFacade: ProductListFacade,
) {}
// Метод реакции на событие из шаблона
onProductSelected(productId: string): void {
// Внутри страницы мы не описываем сложную бизнес-логику
// Здесь только связываем события и вызываем методы из фич/сущностей
this.productListFacade.selectProduct(productId);
}
}
Страница — это «режиссер», который говорит: здесь отрисовать такой список, сюда — кнопку фичи и т.д.
Слой features
Фичи — центральная часть FSD. Они:
- реализуют бизнес‑функции: «добавить в корзину», «авторизоваться», «оформить заказ»;
- имеют свое состояние, логику, UI;
- используются на разных страницах.
Для Angular удобно придерживаться паттерна «фасад + компоненты»:
features/
add-to-cart/
ui/
add-to-cart-button/
add-to-cart-button.component.ts
model/
add-to-cart.facade.ts
add-to-cart.types.ts
index.ts
Покажу вам, как это реализовано на практике.
Фасад:
// features/add-to-cart/model/add-to-cart.facade.ts
import { Injectable } from '@angular/core';
import { CartFacade } from '../../../entities/cart/model/cart.facade';
import { Product } from '../../../entities/product/model/product.types';
@Injectable({
providedIn: 'root', // Или в модуле фичи, если хотите ограничить область
})
export class AddToCartFacade {
constructor(
// Используем сущность Cart для выполнения операции
private readonly cartFacade: CartFacade,
) {}
// Публичный метод фичи - добавить товар в корзину
add(product: Product): void {
// Здесь мы можем описать бизнес-правила фичи
// Например - логировать событие или проверять ограничения
this.cartFacade.addItem({
productId: product.id,
price: product.price,
quantity: 1,
});
}
}
Компонент кнопки:
// features/add-to-cart/ui/add-to-cart-button/add-to-cart-button.component.ts
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { AddToCartFacade } from '../../model/add-to-cart.facade';
import { Product } from '../../../../entities/product/model/product.types';
@Component({
selector: 'app-add-to-cart-button',
templateUrl: './add-to-cart-button.component.html',
styleUrls: ['./add-to-cart-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
})
export class AddToCartButtonComponent {
// Принимаем продукт, для которого отображаем кнопку
@Input() product!: Product;
// Опциональный output для страницы или других компонентов
@Output() added = new EventEmitter<void>();
constructor(
// Внедряем фасад фичи
private readonly addToCartFacade: AddToCartFacade,
) {}
onClick(): void {
// Вызываем бизнес-логику фичи
this.addToCartFacade.add(this.product);
// Сообщаем наружу что действие выполнено
this.added.emit();
}
}
Так фича полностью инкапсулирует сценарий «добавить в корзину». Страница просто вставляет кнопку и передает ей product.
Слой entities
Сущности — это фундамент бизнес‑модели. Они отвечают за:
- данные и их структуру;
- базовые операции (CRUD);
- синхронизацию с API и кеширование.
Структура сущности cart:
entities/
cart/
model/
cart.facade.ts
cart.store.ts
cart.types.ts
cart.api.ts
ui/
cart-widget/
cart-widget.component.ts
index.ts
Фасад корзины:
// entities/cart/model/cart.facade.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { CartItem } from './cart.types';
@Injectable({
providedIn: 'root',
})
export class CartFacade {
// Здесь мы храним текущее состояние корзины
private readonly itemsSubject = new BehaviorSubject<CartItem[]>([]);
// Публичный поток для подписчиков
readonly items$: Observable<CartItem[]> = this.itemsSubject.asObservable();
// Метод для добавления элемента в корзину
addItem(item: CartItem): void {
const items = this.itemsSubject.getValue();
const existing = items.find(i => i.productId === item.productId);
if (existing) {
// Обратите внимание - мы не мутируем массив напрямую
const updated = items.map(i =>
i.productId === item.productId
? { ...i, quantity: i.quantity + item.quantity }
: i,
);
this.itemsSubject.next(updated);
} else {
this.itemsSubject.next([...items, item]);
}
}
// Метод очистки корзины
clear(): void {
this.itemsSubject.next([]);
}
}
Сущность может также содержать работу с API. Важно, чтобы:
- сущность не знала о конкретных страницах;
- сущность могла использоваться разными фичами.
Слой shared
shared — это общий инструментальный слой. Здесь главное не допускать, чтобы в него уходило все подряд.
Чаще всего в shared выделяют:
ui— атомарные и простые UI‑компоненты без жесткой бизнес‑логики (Button,Input,Modal);lib— функции и классы общего назначения (validators,date,http,forms);config— общие конфиги (например, значения изenvironmentв удобной обертке).
Пример shared/ui/button/button.component.ts:
// shared/ui/button/button.component.ts
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'app-button',
template: `
<!-- Универсальная кнопка с несколькими вариациями стилей -->
<button
[ngClass]="['btn', variant]"
[disabled]="disabled"
type="button"
>
<ng-content></ng-content>
</button>
`,
styleUrls: ['./button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
})
export class ButtonComponent {
// Вариант дизайна кнопки
@Input() variant: 'primary' | 'secondary' | 'danger' = 'primary';
// Флаг блокировки кнопки
@Input() disabled = false;
}
Эта кнопка ничего не знает о бизнес‑логике и может использоваться в любом слое.
Правила зависимостей между слоями
Чтобы FSD в Angular был полезен, важно следовать правилам зависимостей. Иначе через несколько месяцев структура снова превратится в хаос.
Типичная матрица зависимостей:
appможет зависеть от всех слоев.processesможет зависеть отpages,features,entities,shared.pagesможет зависеть отfeatures,entities,shared.featuresможет зависеть отentities,shared.entitiesможет зависеть только отshared.sharedне должен зависеть ни от кого выше себя.
Практические советы:
- Не импортируйте страницу в фичу. Если вам кажется, что это нужно, значит фича «знает слишком много».
- Фичи не должны зависеть друг от друга напрямую. Вместо этого вы можете:
- вынести общую часть в сущность;
- или поднять композицию на уровень страницы/процесса.
- Избегайте бизнес‑логики в shared. Shared — не свалка. Если код связан с конкретной сущностью или фичей — поместите его туда.
Публичный API и barrel файлы
FSD очень опирается на идею явных публичных API. В Angular это удобно реализовать через barrel файлы (index.ts), как вы уже видели в примере фичи.
Зачем нужен публичный API
- скрыть внутреннюю структуру модуля;
- регулировать, какие части доступны извне;
- облегчить рефакторинг структуры директорий.
Например, у вас есть фича auth-by-email:
features/
auth-by-email/
ui/
login-form/
register-form/
model/
auth.facade.ts
auth.types.ts
index.ts
index.ts:
// features/auth-by-email/index.ts
// Экспортируем только то, что должно быть доступно другим слоям
export * from './ui/login-form/login-form.component';
export * from './ui/register-form/register-form.component';
export * from './model/auth.facade';
export * from './model/auth.types';
Теперь в странице авторизации импорт выглядит так:
// pages/auth/auth.module.ts
import { LoginFormComponent } from '../../features/auth-by-email';
Если вы решите переименовать внутренние папки, переписывать импорты по всему приложению не придется — достаточно обновить index.ts.
Использование Angular DI в FSD
Инъекция зависимостей (DI) — ключевой механизм Angular. В контексте FSD есть несколько рекомендаций.
Где объявлять провайдеры
Рекомендация:
- сущности и фичи с глобальным состоянием —
providedIn: 'root'или в отдельном модуле слоя; - локальные фичи, которые должны жить в пределах страницы/процесса — провайдеры в модуле страницы или в компоненте (через
providers).
Пример локального фасада фичи, привязанного к странице:
// features/update-profile/model/update-profile.facade.ts
import { Injectable } from '@angular/core';
import { UserFacade } from '../../../entities/user/model/user.facade';
@Injectable()
export class UpdateProfileFacade {
constructor(
private readonly userFacade: UserFacade,
) {}
// Здесь мы описываем операции по обновлению профиля
}
Провайдер в модуле страницы:
// pages/profile/profile.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProfileRoutingModule } from './profile-routing.module';
import { ProfilePageComponent } from './ui/profile-page/profile-page.component';
import { UpdateProfileFacade } from '../../features/update-profile/model/update-profile.facade';
@NgModule({
declarations: [ProfilePageComponent],
imports: [CommonModule, ProfileRoutingModule],
providers: [
// Фасад фичи будет создан в scope этого модуля
UpdateProfileFacade,
],
})
export class ProfileModule {}
Так вы контролируете жизненный цикл и область видимости фич.
Реализация процессов в angular-fsd
Слой processes нужен не всегда. Он особенно полезен, когда у вас есть сложные многошаговые сценарии:
- онбординг пользователя;
- checkout с несколькими шагами;
- wizard для создания сложного объекта.
Пример процесса checkout
Представим процесс checkout:
processes/
checkout/
model/
checkout.facade.ts
ui/
checkout-flow/
checkout-flow.component.ts
index.ts
checkout.facade.ts может объединять:
- данные корзины (
entities/cart); - данные пользователя (
entities/user); - фичи оплаты и доставки (
features/select-delivery,features/select-payment).
Пример фасада процесса:
// processes/checkout/model/checkout.facade.ts
import { Injectable } from '@angular/core';
import { CartFacade } from '../../../entities/cart/model/cart.facade';
import { UserFacade } from '../../../entities/user/model/user.facade';
@Injectable()
export class CheckoutFacade {
constructor(
private readonly cartFacade: CartFacade,
private readonly userFacade: UserFacade,
) {}
// Пример сложного бизнес-сценария
async placeOrder(): Promise<void> {
// Получаем текущие данные корзины и пользователя
const cartItems = await this.cartFacade.items$.pipe(take(1)).toPromise();
const user = await this.userFacade.user$.pipe(take(1)).toPromise();
// Здесь мы могли бы вызвать API оформления заказа
// и обработать различные бизнес-правила
// После успешного оформления очищаем корзину
this.cartFacade.clear();
}
}
Компонент процесса может управлять шагами, состоянием и отображением:
// processes/checkout/ui/checkout-flow/checkout-flow.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CheckoutFacade } from '../../model/checkout.facade';
@Component({
selector: 'app-checkout-flow',
templateUrl: './checkout-flow.component.html',
styleUrls: ['./checkout-flow.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [CheckoutFacade],
})
export class CheckoutFlowComponent {
// Здесь могли бы быть observables с шагами и статусом
constructor(private readonly checkoutFacade: CheckoutFacade) {}
onConfirm(): void {
// Запускаем процесс оформления заказа
this.checkoutFacade.placeOrder();
}
}
Процесс — это «оркестратор» нескольких фич и сущностей.
Как мигрировать существующий Angular проект к FSD
Миграция — самый частый вопрос. Давайте посмотрим, как можно двигаться постепенно.
Шаг 1. Выделить слои
Сначала не трогайте внутренности модулей, а просто:
- Создайте директории
pages,features,entities,shared,processes. - Переместите существующие модули страниц в
pages. - Вынесите наиболее очевидные сущности (User, Product и т.п.) в
entities.
На этом этапе код может быть еще «грязным», но у вас уже появится базовый каркас.
Шаг 2. Ввод публичных API
Далее:
- создайте
index.tsв каждой фиче и сущности; - постепенно переведите импорты на использование этих barrel файлов.
Так вы создадите слой абстракции, который позже облегчит рефакторинг структуры.
Шаг 3. Разделение features и entities
Посмотрите на код страниц:
- все, что связано с конкретным бизнес‑сценарием (например, «подписаться на рассылку») — кандидат в
features; - всё, что описывает объект и его данные (User, Product) — в
entities.
Переносите код постепенно, не пытаясь сделать всё за один раз.
Шаг 4. Ограничение зависимостей
Внедрите простое правило:
- новый код должен соблюдать правила зависимостей FSD;
- старый код трогаете только по мере необходимости.
Можно добавить ESLint‑правила (через import/no-restricted-paths или похожие плагины), которые запрещают «прыгать» через слои.
Практический пример: мини‑фича в angular-fsd
Давайте посмотрим на законченный пример небольшой фичи — подсчет и отображение количества товаров в корзине в хедере.
Задача
В шапке приложения нужно отобразить иконку корзины и число товаров в ней. Логика подсчета уже есть в entities/cart.
Где размещать код
- отображение в хедере — это часть layout (слой
app); - данные о количестве товаров — из
entities/cart; - UI‑элемент корзины в хедере — можно оформить как небольшую фичу
features/cart-indicator.
Структура
features/
cart-indicator/
ui/
cart-indicator/
cart-indicator.component.ts
index.ts
cart-indicator.component.ts:
// features/cart-indicator/ui/cart-indicator/cart-indicator.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { map } from 'rxjs/operators';
import { CartFacade } from '../../../../entities/cart/model/cart.facade';
@Component({
selector: 'app-cart-indicator',
template: `
<!-- Значок корзины с количеством товаров -->
<a [routerLink]="['/cart']" class="cart-indicator">
<span class="cart-indicator__icon">🛒</span>
<span class="cart-indicator__count" *ngIf="count$ | async as count">
{{ count }}
</span>
</a>
`,
styleUrls: ['./cart-indicator.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
})
export class CartIndicatorComponent {
// Здесь мы получаем количество товаров из сущности Cart
readonly count$ = this.cartFacade.items$.pipe(
map(items => items.reduce((sum, item) => sum + item.quantity, 0)),
);
constructor(
private readonly cartFacade: CartFacade,
) {}
}
Потом используем компонент в хедере:
// app/layout/header/header.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CartIndicatorComponent } from '../../../features/cart-indicator';
@Component({
selector: 'app-header',
template: `
<header class="app-header">
<!-- Логотип -->
<a routerLink="/" class="app-header__logo">Store</a>
<!-- Индикатор корзины -->
<app-cart-indicator></app-cart-indicator>
</header>
`,
styleUrls: ['./header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CartIndicatorComponent],
})
export class HeaderComponent {}
Так вы повторно используете сущность cart и при этом сохраняете логику отображения в границах конкретной фичи.
Итог
Подход FSD в Angular (angular-fsd) помогает организовывать код вокруг бизнес‑логики и пользовательских сценариев, а не вокруг технических слоев. Вы:
- делите приложение на слои (
app,processes,pages,features,entities,shared); - ограничиваете зависимости между слоями;
- организуете код фич и сущностей через фасады и публичные API;
- используете модули, DI и lazy loading так, чтобы структура приложения оставалась понятной и масштабируемой.
Ключевая идея — каждый новый файл должен «отвечать» на вопрос: к какой фиче или сущности он относится и в каком слое ему место. Если вы будете отвечать на этот вопрос осознанно, архитектура станет предсказуемой и легче в поддержке.
Частозадаваемые технические вопросы
Как лучше совмещать FSD и NgRx в Angular проекте
Рекомендация — привязывать сторы к сущностям и фичам:
- сторы, описывающие состояние бизнес‑сущности (users, products), размещайте в
entities/<entity>/model; - сторы, описывающие состояние сложной фичи (например, multi-step формы), размещайте в
features/<feature>/model; - избегайте «глобального» стора с огромным корневым стейтом, не привязанным к бизнес‑домену.
Для подключения используйте StoreModule.forFeature в модулях соответствующих слоев (страниц или фич).
Как организовать unit-тесты при использовании FSD
Привязывайте tests к тем же директориям:
entities/user/model/__tests__/user.facade.spec.ts;features/add-to-cart/ui/add-to-cart-button/__tests__/add-to-cart-button.component.spec.ts.
Так вы тестируете компоненты и фасады в контексте их фич/сущностей и упрощаете навигацию по проекту.
Как поступать с глобальными интерцепторами и HTTP‑клиентами
- глобальные HTTP‑интерцепторы и базовую настройку
HttpClientдержите в слоеappилиshared/lib/http; - конкретные API‑сервисы размещайте в
entities/<entity>/model(например,user.api.ts), чтобы они соответствовали доменной модели.
Как делить большие фичи на подфичи в FSD
Если фича разрастается, создайте внутри нее поддиректории:
features/profile/с подфичамиchange-email,change-password,upload-avatar;- при этом каждая подфича имеет свой
ui,model,index.ts.
Снаружи импорт осуществляется через features/profile/change-email и т.п., что сохраняет читаемость структуры.
Как внедрять FSD в монорепо с несколькими Angular приложениями
Выделите общие entities и shared в отдельные библиотеки (например, через Nx или Angular CLI ng g library), а внутри каждой библиотеки сохраните FSD‑структуру директорий. Приложения в монорепо используют эти библиотеки как внешние пакеты с четким публичным API.