Feature-Sliced Design на практике
Практическое руководство по Feature-Sliced Design - архитектуре для масштабируемых фронтенд-приложений. От теории к реальным кейсам.
Оглавление
Feature-Sliced Design на практике
Feature-Sliced Design (FSD) - это не просто папочная структура. Это архитектурная методология, которая делает ваш код предсказуемым, масштабируемым и легко поддерживаемым.
Как пользоваться материалом
- Начинаешь новый проект? Читай разделы про структуру слоёв и правила импортов.
- Рефакторишь legacy код? Изучи миграцию и антипаттерны.
- Работаешь в команде? Сфокусируйся на соглашениях и ESLint правилах.
- Хочешь быстро понять концепцию? Смотри примеры из реальных проектов.
Глоссарий
FSD (Feature-Sliced Design) - Архитектурная методология для фронтенд-проектов, основанная на разделении по слоям и сегментам.
Слой (Layer) - Горизонтальный уровень архитектуры (app, pages, widgets, features, entities, shared).
Сегмент (Segment) - Директория внутри слайса для группировки кода по назначению (ui, model, api, lib, config).
Слайс (Slice) - Вертикальный модуль в рамках слоя, объединяющий логику одной бизнес-сущности.
Public API - Явно экспортируемый интерфейс модуля через index.ts (barrel export).
Barrel Export - Паттерн реэкспорта через index.ts для создания единой точки входа.
Isolation - Принцип изоляции: слой не знает о вышележащих слоях.
Low Coupling - Слабая связанность: минимум зависимостей между модулями.
High Cohesion - Сильная связность внутри модуля: код объединён по смыслу.
Введение: Почему FSD?
Проблемы традиционных подходов
Структура "по типам файлов":
Дерево показывает хаос структуры по типам файлов — так делать не нужно.
src/
components/ ← 200+ файлов
hooks/ ← Куча хуков
utils/ ← Свалка утилит
services/ ← Непонятная логика
❌ Проблемы:
- Невозможно найти код фичи
- Всё связано со всем
- Сложно удалить фичу (файлы разбросаны)
- Нет понятных границ ответственности
Структура "по фичам" (наивная):
Дерево с наивной структурой по фичам демонстрирует отсутствие правил.
src/
features/
user-profile/ ← Что здесь? UI? Логика? API?
payments/
notifications/
❌ Проблемы:
- Нет единых соглашений
- Переиспользуемость страдает
- Циклические зависимости
- Каждый делает как хочет
FSD как решение
Стандартная целевая структура слоёв FSD для сравнения.
src/
app/ ← Инициализация приложения
pages/ ← Страницы (роутинг)
widgets/ ← Композиции фич
features/ ← Пользовательские сценарии
entities/ ← Бизнес-сущности
shared/ ← Переиспользуемый код
✅ Преимущества:
- Понятная структура для любого разработчика
- Строгие правила импортов (нет спагетти)
- Легко масштабировать команду
- Простое удаление фич
- Переиспользование без дублирования
Слои FSD: Подробно
Главное правило FSD: слои импортируют только нижележащие слои. Это как слоёный пирог - верхние слои опираются на нижние, но не наоборот.
1. Shared - Фундамент
Назначение: Переиспользуемый код без бизнес-логики.
Что здесь:
- UI-компоненты (Button, Input, Card)
- Утилиты (cn, formatDate, validators)
- API клиенты (axios, fetch обёртки)
- Конфиги (константы, env)
- Типы общего назначения
Структура:
Скелет содержимого слоя shared, чтобы видеть, что считается фундаментом.
shared/
ui/ ← UI-примитивы
button/
button.tsx
button.test.tsx
index.ts ← Barrel export
lib/ ← Утилиты
cn.ts
format.ts
api/ ← API базовый слой
client.ts
types.ts
config/ ← Конфигурация
routes.ts
constants.ts
icons/ ← Иконки
index.ts
Пример: UI компонент
Реализация UI-примитива Button показывает, как выглядит код в shared.
// shared/ui/button/button.tsx
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/shared/lib";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
outline: "border border-input bg-background hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}Barrel shared/ui фиксирует публичный API примитивов.
// shared/ui/index.ts (Public API)
export { Button, type ButtonProps } from "./button/button";
export { Input, type InputProps } from "./input/input";
export { Card, CardHeader, CardContent } from "./card/card";Пример: Утилиты
Утилита cn объединяет классы — пример простого shared/lib.
// shared/lib/cn.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}Форматтеры дат и чисел как типичные shared/lib функции.
// shared/lib/format.ts
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat("ru-RU", {
day: "2-digit",
month: "long",
year: "numeric",
}).format(date);
}
export function formatNumber(num: number): string {
return new Intl.NumberFormat("ru-RU").format(num);
}Barrel shared/lib показывает, что именно мы раскрываем наружу.
// shared/lib/index.ts (Public API)
export { cn } from "./cn";
export { formatDate, formatNumber } from "./format";2. Entities - Бизнес-сущности
Назначение: Бизнес-модели без пользовательских сценариев.
Что здесь:
- Модели данных (User, Post, Product)
- Минимальный UI (карточки, списки)
- API методы для CRUD
- Типы и схемы валидации
Структура:
Дерево entities иллюстрирует, как группировать бизнес-сущности.
entities/
user/
model/
types.ts ← Типы User
schema.ts ← Zod/Yup схемы
api/
user-api.ts ← API методы
ui/
user-card.tsx ← Карточка пользователя
index.ts ← Public API
post/
model/
api/
ui/
index.ts
Пример: Entity User
Типы User описывают модель сущности в entities.
// entities/user/model/types.ts
export type User = {
id: string;
email: string;
username: string;
avatar?: string;
role: "user" | "admin";
createdAt: Date;
};
export type UserProfile = User & {
bio?: string;
website?: string;
location?: string;
};userApi показывает, что CRUD живёт внутри сущности.
// entities/user/api/user-api.ts
import { apiClient } from "@/shared/api";
import type { User } from "../model/types";
export const userApi = {
async getById(id: string): Promise<User> {
const response = await apiClient.get<User>(`/users/${id}`);
return response.data;
},
async getAll(): Promise<User[]> {
const response = await apiClient.get<User[]>("/users");
return response.data;
},
async update(id: string, data: Partial<User>): Promise<User> {
const response = await apiClient.patch<User>(`/users/${id}`, data);
return response.data;
},
};UserCard демонстрирует минимальный UI для сущности без бизнес-логики.
// entities/user/ui/user-card.tsx
import { Card } from "@/shared/ui";
import type { User } from "../model/types";
export function UserCard({ user }: { user: User }) {
return (
<Card>
<div className="flex items-center gap-3">
{user.avatar && (
<img
src={user.avatar}
alt={user.username}
className="h-12 w-12 rounded-full"
/>
)}
<div>
<h3 className="font-semibold">{user.username}</h3>
<p className="text-muted-foreground text-sm">{user.email}</p>
</div>
</div>
</Card>
);
}Barrel entities/user фиксирует публичный API сущности.
// entities/user/index.ts (Public API)
export type { User, UserProfile } from "./model/types";
export { userApi } from "./api/user-api";
export { UserCard } from "./ui/user-card";3. Features - Пользовательские сценарии
Назначение: Действия пользователя (добавить в корзину, лайкнуть, войти).
Что здесь:
- Формы и кнопки действий
- Модалки и диалоги
- Бизнес-логика сценариев
- Обработка событий
Структура:
Структура feature login раскрывает разбиение фичи по сегментам.
features/
auth/
login-form/
ui/
login-form.tsx
model/
use-login.ts
index.ts
logout-button/
ui/
index.ts
post/
create-post/
like-post/
share-post/
Пример: Feature Login
Компонент LoginForm показывает UI сценария авторизации.
// features/auth/login-form/ui/login-form.tsx
"use client";
import { useState } from "react";
import { Button, Input } from "@/shared/ui";
import { useLogin } from "../model/use-login";
export function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const { login, isLoading, error } = useLogin();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await login({ email, password });
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<Input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<Input
type="password"
placeholder="Пароль"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
{error && <p className="text-danger text-sm">{error}</p>}
<Button type="submit" disabled={isLoading} className="w-full">
{isLoading ? "Вход..." : "Войти"}
</Button>
</form>
);
}Хук useLogin хранит бизнес-логику сценария и работу с API.
// features/auth/login-form/model/use-login.ts
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { authApi } from "@/shared/api";
type LoginCredentials = {
email: string;
password: string;
};
export function useLogin() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const login = async (credentials: LoginCredentials) => {
setIsLoading(true);
setError(null);
try {
const { token } = await authApi.login(credentials);
localStorage.setItem("auth_token", token);
router.push("/dashboard");
} catch (err) {
setError(err instanceof Error ? err.message : "Ошибка входа");
} finally {
setIsLoading(false);
}
};
return { login, isLoading, error };
}Barrel login-form экспортирует публичный интерфейс фичи.
// features/auth/login-form/index.ts
export { LoginForm } from "./ui/login-form";4. Widgets - Композиции
Назначение: Самодостаточные блоки страниц из features и entities.
Что здесь:
- Хедер, футер, сайдбар
- Комплексные секции страниц
- Композиции фич
Структура:
Структура widgets демонстрирует, какие блоки собирают фичи.
widgets/
site-header/
ui/
site-header.tsx
nav.tsx
config/
navigation.ts
index.ts
site-footer/
product-grid/
user-dashboard/
Пример: Widget SiteHeader
SiteHeader показывает композицию фич и сущностей в виджете.
// widgets/site-header/ui/site-header.tsx
"use client";
import Link from "next/link";
import { Button } from "@/shared/ui";
import { navigation } from "../config/navigation";
import { LogoutButton } from "@/features/auth";
export function SiteHeader() {
return (
<header className="bg-background/95 sticky top-0 z-50 w-full border-b backdrop-blur">
<div className="container flex h-16 items-center justify-between">
<Link href="/" className="text-xl font-bold">
Logo
</Link>
<nav className="hidden gap-6 md:flex">
{navigation.map((item) => (
<Link
key={item.href}
href={item.href}
className="hover:text-primary text-sm font-medium transition-colors"
>
{item.label}
</Link>
))}
</nav>
<div className="flex items-center gap-4">
<Button variant="ghost" asChild>
<Link href="/login">Войти</Link>
</Button>
<LogoutButton />
</div>
</div>
</header>
);
}navigation.ts хранит конфигурацию ссылок для хедера.
// widgets/site-header/config/navigation.ts
export const navigation = [
{ label: "Главная", href: "/" },
{ label: "Проекты", href: "/projects" },
{ label: "Блог", href: "/blog" },
{ label: "Контакты", href: "/contacts" },
] as const;Barrel виджета хедера ограничивает публичные экспорты.
// widgets/site-header/index.ts
export { SiteHeader } from "./ui/site-header";5. Pages - Страницы (в App Router)
Назначение: Композиция виджетов для конкретных роутов.
В Next.js App Router pages слой обычно находится в app/ директории:
Главная страница демонстрирует сборку виджетов в роуте.
// app/page.tsx (главная страница)
import { SiteHeader, Hero, FeaturedProjects, SiteFooter } from "@/widgets";
export default function HomePage() {
return (
<>
<SiteHeader />
<main>
<Hero />
<FeaturedProjects />
</main>
<SiteFooter />
</>
);
}Страница каталога Projects иллюстрирует другую композицию виджетов.
// app/projects/page.tsx
import { SiteHeader, ProjectsGrid, SiteFooter } from "@/widgets";
export default function ProjectsPage() {
return (
<>
<SiteHeader />
<main>
<ProjectsGrid />
</main>
<SiteFooter />
</>
);
}6. App - Инициализация приложения
Назначение: Глобальная конфигурация, провайдеры, стили.
Layout приложения показывает, где живут метаданные и глобальные стили.
// app/layout.tsx
import type { Metadata } from "next";
import { Providers } from "./providers";
import "./globals.css";
export const metadata: Metadata = {
title: "My App",
description: "Built with FSD",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ru">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Providers иллюстрирует подключение глобальных провайдеров.
// app/providers.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider } from "next-themes";
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider attribute="data-theme" defaultTheme="system">
{children}
</ThemeProvider>
</QueryClientProvider>
);
}Правила импортов
Нарушение правил импортов - самая частая ошибка при использовании FSD. Настройте ESLint, чтобы ловить их автоматически!
Главное правило: направление импортов
Импорты идут только вниз по слоям:
Сводная таблица показывает допустимые направления импортов между слоями.
app ⬇️ может импортировать всё ниже
pages ⬇️ может импортировать widgets, features, entities, shared
widgets ⬇️ может импортировать features, entities, shared
features ⬇️ может импортировать entities, shared
entities ⬇️ может импортировать только shared
shared ⬇️ ничего не импортирует (кроме внутренних модулей)
✅ Правильные импорты
ProductCard пример корректного импорта features/entities из виджета.
// ✅ widgets импортирует features и entities
// widgets/product-card/ui/product-card.tsx
import { Button } from "@/shared/ui";
import { Product } from "@/entities/product";
import { AddToCart } from "@/features/cart";
export function ProductCard({ product }: { product: Product }) {
return (
<div>
<h3>{product.name}</h3>
<AddToCart productId={product.id} />
</div>
);
}AddToCartButton показывает правильный импорт entities из feature.
// ✅ features импортирует entities
// features/cart/add-to-cart/ui/add-to-cart-button.tsx
import { Button } from "@/shared/ui";
import { Product } from "@/entities/product";
export function AddToCartButton({ product }: { product: Product }) {
return <Button>Добавить {product.name}</Button>;
}❌ Неправильные импорты
Антипример: entity тянет feature, нарушая слой.
// ❌ entities НЕ МОЖЕТ импортировать features
// entities/product/ui/product-card.tsx
import { AddToCart } from "@/features/cart"; // ❌ ОШИБКА!
export function ProductCard() {
return (
<div>
<AddToCart /> {/* Это нарушает изоляцию слоёв */}
</div>
);
}Антипример: feature импортирует другую feature напрямую.
// ❌ features НЕ МОЖЕТ импортировать другие features напрямую
// features/cart/model/use-cart.ts
import { useAuth } from "@/features/auth"; // ❌ ОШИБКА!
// Решение: композиция на уровне widgets или pagesАнтипример: shared импортирует entity и нарушает изоляцию.
// ❌ shared НЕ МОЖЕТ импортировать entities
// shared/ui/user-avatar.tsx
import { User } from "@/entities/user"; // ❌ ОШИБКА!
// Решение: переместить в entities/user/ui/Public API через barrel exports
Обязательное правило: Все импорты идут через index.ts.
Блок сравнивает deep imports с правильными barrel-импортами.
// ❌ Deep imports (напрямую в файл) - ЗАПРЕЩЕНЫ
import { Button } from "@/shared/ui/button/button";
import { UserCard } from "@/entities/user/ui/user-card";
// ✅ Barrel imports (через index.ts) - ПРАВИЛЬНО
import { Button } from "@/shared/ui";
import { UserCard } from "@/entities/user";Пример Public API:
Пример публичного API user показывает, что оставляем наружу.
// entities/user/index.ts
// Это единственная точка входа в модуль user
export type { User, UserProfile } from "./model/types";
export { userApi } from "./api/user-api";
export { UserCard } from "./ui/user-card";
export { UserList } from "./ui/user-list";
// ❌ НЕ экспортируем внутренние детали:
// export { validateEmail } from "./lib/validate"; // internal onlyНастройка ESLint для FSD
Конфиг ESLint иллюстрирует запрет глубоких импортов.
// eslint.config.mjs
import { FlatCompat } from "@eslint/eslintrc";
const compat = new FlatCompat({
baseDirectory: import.meta.dirname,
});
export default [
...compat.config({
rules: {
// Запрет deep imports
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["@/shared/ui/*", "!@/shared/ui"],
message: "Use barrel import: @/shared/ui",
},
{
group: ["@/shared/lib/*", "!@/shared/lib"],
message: "Use barrel import: @/shared/lib",
},
{
group: ["@/entities/*/**"],
message: "Use barrel import: @/entities/{entity}",
},
{
group: ["@/features/*/**"],
message: "Use barrel import: @/features/{feature}/{slice}",
},
{
group: ["@/widgets/*/**"],
message: "Use barrel import: @/widgets/{widget}",
},
],
},
],
},
}),
];Управление состоянием в FSD
Server State живёт в entities, Client State — в features и widgets. Не
создавайте анонимных shared/stores.
Server State (данные с бекенда)
- Где хранить:
entities/{entity}/api+ хелперы вmodel. - Инструмент:
@tanstack/react-query, RTK Query или SWR — выбирайте, но не смешивайте. - Query Keys: рядом с сущностью (
entities/product/api/query-keys.ts) или инлайн в хуке. - Мутации: в
entities/{entity}/api, экспортируются какuseUpdateUser,useCreateOrder.
Query keys и useWorkspace показывают, как хранить server state в entities.
// entities/workspace/api/query-keys.ts
export const workspaceKeys = {
all: ["workspace"] as const,
list: () => [...workspaceKeys.all, "list"] as const,
byId: (id: string) => [...workspaceKeys.all, "detail", id] as const,
};
// entities/workspace/api/use-workspace.ts
import { useQuery } from "@tanstack/react-query";
import { apiClient } from "@/shared/api";
import { workspaceKeys } from "./query-keys";
export function useWorkspace(id: string) {
return useQuery({
queryKey: workspaceKeys.byId(id),
queryFn: () => apiClient.get(`/workspaces/${id}`).then((r) => r.data),
staleTime: 60_000,
});
}Client State (UI, модалки, формы)
- Где хранить: Внутри фич (
features/.../model) или виджетов (widgets/.../model). - Инструмент: локальный
useState,useReducerили Zustand-стор, если состояние нужно за пределами одного компонента. - Пересечение фич: Создавайте стор внутри слоя, который их объединяет (обычно
widgets). Не заводите глобальный стор вshared.
Zustand-стор order-flow пример client state внутри виджета.
// widgets/order-flow/model/order-store.ts
import { create } from "zustand";
import { devtools } from "zustand/middleware";
type State = {
step: "shipping" | "payment" | "summary";
selectedShipping?: string;
};
type Actions = {
nextStep: () => void;
setShipping: (id: string) => void;
};
export const useOrderFlowStore = create<State & Actions>()(
devtools((set, get) => ({
step: "shipping",
setShipping: (id) => set({ selectedShipping: id }),
nextStep: () =>
set({ step: get().step === "shipping" ? "payment" : "summary" }),
}))
);OrderFlow компонент показывает использование стора и данных сущности.
// widgets/order-flow/ui/order-flow.tsx
import { useOrderFlowStore } from "../model/order-store";
import { useOrder } from "@/entities/order";
export function OrderFlow({ orderId }: { orderId: string }) {
const { data: order } = useOrder(orderId);
const step = useOrderFlowStore((s) => s.step);
if (!order) return null;
return step === "shipping" ? (
<ShippingForm />
) : step === "payment" ? (
<PaymentForm />
) : (
<Summary order={order} />
);
}Где хранить «общий» стейт
- Выбор Workspace:
entities/workspace/model(серверные данные + выбранный workspace) экспортируетuseActiveWorkspace. - Глобальные UI: провайдеры в
app/providers.tsx, хук управления — вshared/lib(без бизнес-логики). - Формы: сложные формы — локальный Zustand/Reducer в
features/{feature}/model.
Антипаттерн: shared/stores/useAppStore.ts с состоянием всех модалок и фильтров. Делите по слоям и доменам.
SSR и Next.js
- Серверные компоненты тянут данные через
entities/{entity}/apiсfetch/revalidateи передают их в клиентские виджеты через пропсы. - Для React Query: делайте
prefetchQuery/dehydrateв серверном компоненте страницы и кладитеdehydratedStateв провайдер внутриapp/.
Реальные примеры из проектов
Кейс 1: E-commerce (интернет-магазин)
Структура:
Дерево e-commerce проекта иллюстрирует распределение слоёв.
src/
shared/
ui/ ← Button, Input, Card, Badge
lib/ ← formatPrice, cn
api/ ← apiClient
entities/
product/ ← Product модель, ProductCard
category/ ← Category модель, CategoryTag
user/ ← User, UserProfile
cart/ ← CartItem модель (данные)
features/
product/
add-to-cart/ ← Кнопка "В корзину"
product-filters/ ← Фильтры товаров
auth/
login-form/
register-form/
checkout/
checkout-form/
payment-form/
widgets/
product-grid/ ← Сетка товаров + фильтры
cart-sidebar/ ← Боковая корзина
header/
footer/
app/
page.tsx ← Главная
products/
page.tsx ← Каталог
[id]/page.tsx ← Карточка товара
cart/page.tsx ← Корзина
Пример композиции:
ProductGrid пример композиции фич и сущностей в виджете каталога.
// widgets/product-grid/ui/product-grid.tsx
import { ProductCard } from "@/entities/product";
import { ProductFilters } from "@/features/product/product-filters";
import { AddToCart } from "@/features/product/add-to-cart";
export function ProductGrid() {
const products = useProducts();
return (
<div>
<ProductFilters />
<div className="grid grid-cols-3 gap-4">
{products.map((product) => (
<div key={product.id}>
<ProductCard product={product} />
<AddToCart productId={product.id} />
</div>
))}
</div>
</div>
);
}Кейс 2: SaaS Dashboard
Структура:
Структура SaaS dashboard показывает FSD для B2B.
src/
shared/
ui/
lib/
api/
entities/
workspace/ ← Workspace модель
project/ ← Project модель
task/ ← Task модель
user/
features/
workspace/
create-workspace/
switch-workspace/
project/
create-project/
archive-project/
task/
create-task/
assign-task/
task-status-toggle/
widgets/
workspace-sidebar/
project-kanban/
task-list/
stats-dashboard/
app/
dashboard/
page.tsx
projects/[id]/page.tsx
Кейс 3: Блог-платформа
Структура:
Структура блог-платформы демонстрирует применимость для контента.
src/
shared/
ui/
lib/
entities/
post/ ← Post модель, PostCard
author/ ← Author модель
comment/ ← Comment модель
tag/ ← Tag модель
features/
post/
create-post/
edit-post/
like-post/
share-post/
comment/
add-comment/
reply-to-comment/
subscription/
subscribe-button/
widgets/
post-feed/
trending-sidebar/
author-card/
comments-section/
Частые антипаттерны и ошибки
Эти ошибки встречаются даже у опытных разработчиков. Изучите их, чтобы не повторять!
❌ Антипаттерн 1: "Бог-компонент" в shared
Плохо:
Плохой пример бог-компонента в shared, чтобы показать антипаттерн.
// shared/ui/user-profile-card.tsx
import { userApi } from "@/entities/user"; // ❌
import { FollowButton } from "@/features/user/follow"; // ❌
export function UserProfileCard({ userId }: { userId: string }) {
const user = await userApi.getById(userId); // Бизнес-логика в shared!
return (
<div>
<h3>{user.name}</h3>
<FollowButton userId={userId} />
</div>
);
}Почему плохо:
- shared импортирует entities и features
- Компонент знает о бизнес-логике
- Нельзя переиспользовать без этих зависимостей
Хорошо:
Card-примитив иллюстрирует правильное место UI без бизнес-логики.
// shared/ui/card.tsx (примитив)
export function Card({ children }: { children: React.ReactNode }) {
return <div className="rounded-lg border p-4">{children}</div>;
}
// entities/user/ui/user-card.tsx
import { Card } from "@/shared/ui";
import type { User } from "../model/types";
export function UserCard({
user,
actions,
}: {
user: User;
actions?: React.ReactNode;
}) {
return (
<Card>
<h3>{user.name}</h3>
{actions}
</Card>
);
}
// widgets/user-profile/ui/user-profile.tsx
import { UserCard } from "@/entities/user";
import { FollowButton } from "@/features/user/follow";
export function UserProfile({ userId }: { userId: string }) {
const user = useUser(userId);
return <UserCard user={user} actions={<FollowButton userId={userId} />} />;
}❌ Антипаттерн 2: Циклические зависимости между features
Плохо:
Антипример циклических зависимостей между фичами.
// features/auth/model/use-auth.ts
import { clearCart } from "@/features/cart"; // ❌
export function useAuth() {
const logout = () => {
clearCart(); // feature зависит от другой feature
// ...
};
}
// features/cart/model/use-cart.ts
import { isAuthenticated } from "@/features/auth"; // ❌
export function useCart() {
if (!isAuthenticated()) {
// циклическая зависимость
// ...
}
}Хорошо:
Вариант 1: Композиция на уровне widgets/pages
UserMenu демонстрирует композицию на уровне виджета вместо связи фич.
// widgets/user-menu/ui/user-menu.tsx
import { LogoutButton } from "@/features/auth";
import { useCart } from "@/features/cart";
export function UserMenu() {
const { clearCart } = useCart();
const handleLogout = async () => {
await clearCart(); // Композиция здесь, не в features
await logout();
};
return <button onClick={handleLogout}>Выйти</button>;
}Вариант 2: Общая логика в entities
sessionModel показывает перенос общей логики в entities.
// entities/session/model/session.ts
export const sessionModel = {
isAuthenticated: () => Boolean(localStorage.getItem("token")),
// ...
};
// features/cart используют entities/session, а не features/auth
import { sessionModel } from "@/entities/session";❌ Антипаттерн 3: Раздутый index.ts
Плохо:
Раздутый shared/ui/index.ts как антипример barrel.
// shared/ui/index.ts (500+ строк)
export * from "./button/button";
export * from "./input/input";
export * from "./card/card";
// ... 100+ компонентов
export * from "./complex-form/form"; // ❌ Сложная форма в shared?
export * from "./user-dashboard/dashboard"; // ❌ Dashboard в shared?Почему плохо:
- Долгая компиляция
- Сложно найти нужный экспорт
- Бизнес-логика просочилась в shared
Хорошо:
Минимальный shared/ui/index.ts как правильный API.
// shared/ui/index.ts (минимальный)
// Экспортируем только UI-примитивы
export { Button } from "./button/button";
export { Input } from "./input/input";
export { Card, CardHeader, CardContent } from "./card/card";
export { Badge } from "./badge/badge";
// Сложные формы → features
// features/auth/login-form/index.ts
export { LoginForm } from "./ui/login-form";
// Dashboard → widgets
// widgets/user-dashboard/index.ts
export { UserDashboard } from "./ui/user-dashboard";❌ Антипаттерн 4: Переиспользование через copy-paste
Плохо:
LikeButton в feature post демонстрирует переиспользование примитива.
// features/post/like-post/ui/like-button.tsx
export function LikePostButton() {
return (
<button className="bg-primary flex items-center gap-2 rounded-lg px-4 py-2">
<HeartIcon /> Нравится
</button>
);
}
// features/comment/like-comment/ui/like-button.tsx
export function LikeCommentButton() {
return (
<button className="bg-primary flex items-center gap-2 rounded-lg px-4 py-2">
<HeartIcon /> Нравится
</button>
);
}Хорошо:
LikeButton примитив выносит общую кнопку в shared.
// shared/ui/like-button/like-button.tsx
export function LikeButton({
onClick,
isLiked,
}: {
onClick: () => void;
isLiked?: boolean;
}) {
return (
<button
onClick={onClick}
className={cn(
"flex items-center gap-2 rounded-lg px-4 py-2",
isLiked ? "bg-danger" : "bg-primary"
)}
>
<HeartIcon className={isLiked ? "fill-current" : ""} />
Нравится
</button>
);
}
// features/post/like-post/ui/like-post-button.tsx
import { LikeButton } from "@/shared/ui";
export function LikePostButton({ postId }: { postId: string }) {
const { like, isLiked } = useLikePost(postId);
return <LikeButton onClick={like} isLiked={isLiked} />;
}
// features/comment/like-comment/ui/like-comment-button.tsx
import { LikeButton } from "@/shared/ui";
export function LikeCommentButton({ commentId }: { commentId: string }) {
const { like, isLiked } = useLikeComment(commentId);
return <LikeButton onClick={like} isLiked={isLiked} />;
}Миграция на FSD
Не нужно переписывать всё сразу! Мигрируйте постепенно, модуль за модулем.
Стратегия миграции
Этап 1: Подготовка
- Создайте структуру папок FSD
- Настройте алиасы в
tsconfig.json - Настройте ESLint правила
Этап 2: Миграция shared
Команда «Было» показывает перенос в shared/entities при миграции.
# Было
src/components/ui/Button.tsx
src/utils/cn.ts
# Стало
src/shared/ui/button/button.tsx
src/shared/lib/cn.tsЭтап 3: Миграция entities
Команда «Стало» для следующего шага миграции демонстрирует структуру.
# Было
src/models/User.ts
src/components/UserCard.tsx
src/api/userApi.ts
# Стало
src/entities/user/
model/types.ts
ui/user-card.tsx
api/user-api.ts
index.tsЭтап 4: Выделение features
Антипример цельного компонента до разделения на фичи.
// Было: всё в одном компоненте
function UserProfile() {
const handleFollow = () => {
/* ... */
};
const handleMessage = () => {
/* ... */
};
return (
<div>
<UserCard />
<button onClick={handleFollow}>Подписаться</button>
<button onClick={handleMessage}>Написать</button>
</div>
);
}
// Стало: разделено на features
function UserProfile() {
return (
<div>
<UserCard />
<FollowButton /> {/* features/user/follow-button */}
<MessageButton /> {/* features/user/message-button */}
</div>
);
}Инкрементальная миграция
Используйте Strangler Fig Pattern - постепенно заменяйте старый код:
Шаг 1 миграции: реэкспорт из старого места для совместимости.
// Шаг 1: Создали новый модуль в FSD
// entities/user/index.ts
export { UserCard } from "./ui/user-card";
// Шаг 2: Реэкспортируем из старого места (для обратной совместимости)
// src/components/UserCard.tsx (deprecated)
export { UserCard } from "@/entities/user";
console.warn("Import from @/entities/user instead");
// Шаг 3: Постепенно обновляем импорты
// Было
import { UserCard } from "@/components/UserCard";
// Стало
import { UserCard } from "@/entities/user";
// Шаг 4: Удаляем старый файл когда все импорты обновленыBest Practices
1. Сегменты (segments) внутри слайсов
Стандартные сегменты:
Шаблон сегментов демонстрирует стандартное содержимое каждой фичи.
feature-name/
ui/ ← React компоненты
model/ ← Бизнес-логика, хуки, стор
api/ ← API запросы
lib/ ← Вспомогательные функции (только для этого слайса)
config/ ← Конфигурация, константы
types/ ← TypeScript типы (если много)
index.ts ← Public API
Пример:
Пример сегментов на реальной фиче auth.
features/
auth/
login-form/
ui/
login-form.tsx
login-form.test.tsx
model/
use-login.ts
validation.ts
api/
auth-api.ts
config/
constants.ts
index.ts
2. Композиция через слоты
Используйте слоты для гибкости:
ProductCard со слотами показывает композицию через пропсы.
// entities/product/ui/product-card.tsx
export function ProductCard({
product,
actions, // ← слот для actions
badge, // ← слот для badge
}: {
product: Product;
actions?: React.ReactNode;
badge?: React.ReactNode;
}) {
return (
<Card>
{badge}
<h3>{product.name}</h3>
<p>{product.price}</p>
{actions}
</Card>
);
}
// widgets/product-grid/ui/product-item.tsx
import { ProductCard } from "@/entities/product";
import { AddToCart } from "@/features/cart/add-to-cart";
import { Badge } from "@/shared/ui";
<ProductCard
product={product}
badge={product.isNew ? <Badge>Новинка</Badge> : null}
actions={<AddToCart productId={product.id} />}
/>;3. Dependency Injection для переиспользования
NotificationList демонстрирует DI для переиспользования рендера.
// entities/notification/ui/notification-list.tsx
export function NotificationList({
notifications,
renderNotification, // ← DI для кастомного рендера
}: {
notifications: Notification[];
renderNotification?: (n: Notification) => React.ReactNode;
}) {
return (
<ul>
{notifications.map((n) =>
renderNotification ? (
renderNotification(n)
) : (
<NotificationCard key={n.id} notification={n} />
)
)}
</ul>
);
}
// widgets/user-notifications/ui/user-notifications.tsx
<NotificationList
notifications={notifications}
renderNotification={(n) => (
<CustomNotification notification={n} onDismiss={handleDismiss} />
)}
/>;4. Тестирование в FSD
Тесты рядом с кодом:
Схема расположения тестов рядом с кодом.
features/
auth/
login-form/
ui/
login-form.tsx
login-form.test.tsx ← Unit тест компонента
model/
use-login.ts
use-login.test.ts ← Unit тест хука
__tests__/
login-form.integration.test.tsx ← Интеграционный тест
Пример теста:
Тест LoginForm иллюстрирует unit проверку компонента.
// features/auth/login-form/ui/login-form.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./login-form";
describe("LoginForm", () => {
it("should submit form with credentials", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText("Email"), "test@example.com");
await user.type(screen.getByLabelText("Пароль"), "password123");
await user.click(screen.getByRole("button", { name: "Войти" }));
expect(onSubmit).toHaveBeenCalledWith({
email: "test@example.com",
password: "password123",
});
});
});5. Документация через README
Добавляйте README в сложные модули:
Пример структуры с README в фиче checkout.
features/
checkout/
README.md ← Документация фичи
payment-form/
shipping-form/
order-summary/
README-файл показывает, как документировать фичу.
# Checkout Feature
Оформление заказа в 3 шага:
1. Адрес доставки (shipping-form)
2. Способ оплаты (payment-form)
3. Подтверждение (order-summary)
## Использование
\`\`\`tsx
import { CheckoutWizard } from "@/features/checkout";
<CheckoutWizard onComplete={handleOrderComplete} />
\`\`\`
## API
- `CheckoutWizard` - основной компонент
- `useCheckout` - хук для управления состояниемИнструменты и линтинг
Path Aliases (tsconfig.json)
Alias-настройки в tsconfig демонстрируют пути для слоёв.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/app/*": ["app/*"],
"@/widgets/*": ["src/widgets/*"],
"@/features/*": ["src/features/*"],
"@/entities/*": ["src/entities/*"],
"@/shared/*": ["src/shared/*"],
// Barrel aliases (для удобства)
"@/shared/ui": ["src/shared/ui/index.ts"],
"@/shared/lib": ["src/shared/lib/index.ts"],
"@/entities/user": ["src/entities/user/index.ts"],
"@/entities/product": ["src/entities/product/index.ts"]
}
}
}ESLint: запрет deep imports
ESLint пример упрощённого запрета deep imports.
// eslint.config.mjs
export default [
{
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["@/shared/ui/*", "!@/shared/ui"],
message: "Import from @/shared/ui barrel instead",
},
{
group: ["@/entities/*/**"],
message: "Import from @/entities/{entity} barrel instead",
},
],
},
],
},
},
];Steiger - автоматический анализ FSD
Команда установки Steiger показывает как подключить линтер.
npm install -D steiger @steiger/eslint-plugin
# Запуск анализа
npx steiger srcSteiger находит:
- Нарушения правил импортов между слоями
- Циклические зависимости
- Отсутствующие Public API (index.ts)
- Неиспользуемые модули
FSD в аду: реалии продакшена
Фича upload-document: прогресс, ошибки, повтор
Дерево фичи upload-document показывает сегменты сложной фичи.
features/
documents/
upload-document/
ui/upload-document.tsx
model/use-upload-document.ts
lib/validators.ts
index.ts
Компонент UploadDocument иллюстрирует работу с прогрессом/повтором.
// features/documents/upload-document/ui/upload-document.tsx
"use client";
import { useUploadDocument } from "../model/use-upload-document";
import { Button, Input } from "@/shared/ui";
export function UploadDocument() {
const { selectFile, progress, error, retry, isUploading } =
useUploadDocument();
return (
<div className="space-y-3">
<Input type="file" accept=".pdf,.doc,.png" onChange={selectFile} />
{progress !== null && (
<div className="bg-muted h-2 rounded">
<div
className="bg-primary h-full rounded"
style={{ width: `${progress}%` }}
/>
</div>
)}
{error && (
<div className="text-danger flex items-center gap-2 text-sm">
Ошибка: {error}
<Button size="sm" variant="outline" onClick={retry}>
Повторить
</Button>
</div>
)}
<Button disabled={isUploading} onClick={retry}>
{isUploading ? `Загружено ${progress ?? 0}%` : "Загрузить"}
</Button>
</div>
);
}Хук useUploadDocument показывает обработку валидации и прогресса.
// features/documents/upload-document/model/use-upload-document.ts
"use client";
import { useState, useCallback } from "react";
import { documentsApi } from "@/shared/api";
import { validateFile } from "../lib/validators";
export function useUploadDocument() {
const [file, setFile] = useState<File | null>(null);
const [progress, setProgress] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);
const [isUploading, setUploading] = useState(false);
const upload = useCallback(async (fileToUpload: File) => {
setUploading(true);
setError(null);
setProgress(0);
const controller = new AbortController();
try {
validateFile(fileToUpload);
await documentsApi.upload(fileToUpload, {
signal: controller.signal,
onProgress: (v) => setProgress(v), // прогресс 0-100
});
setProgress(100);
} catch (err) {
setError(
err instanceof Error ? err.message : "Не удалось загрузить файл"
);
} finally {
setUploading(false);
}
}, []);
const selectFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const nextFile = e.target.files?.[0];
if (!nextFile) return;
setFile(nextFile);
void upload(nextFile);
};
const retry = () => file && void upload(file);
return { selectFile, retry, progress, error, isUploading };
}Миграция легаси-компонента OrderSummary
Дерево миграции легаси OrderSummary показывает изоляцию.
widgets/
order-summary/
ui/order-summary.tsx
lib/legacy-adapter.tsx ← единственная точка контакта с монолитом
index.ts
legacy/
order/OrderSummary.jsx ← чужой код, не трогаем
LegacyOrderSummaryAdapter инкапсулирует сторонний компонент.
// widgets/order-summary/lib/legacy-adapter.tsx
// Изолируем все зависимости легаси внутри адаптера
import LegacyOrderSummary from "@/legacy/order/OrderSummary";
type LegacyProps = { onSubmit: () => void; locale: string; customerId: string };
export function LegacyOrderSummaryAdapter({
onConfirm,
orderId,
}: {
onConfirm: () => void;
orderId: string;
}) {
// маппим пропсы и скрываем грязную API-форму
const props: LegacyProps = {
onSubmit: onConfirm,
locale: "ru",
customerId: orderId,
};
return <LegacyOrderSummary {...props} />;
}OrderSummary виджет показывает чистый API поверх легаси.
// widgets/order-summary/ui/order-summary.tsx
import { LegacyOrderSummaryAdapter } from "../lib/legacy-adapter";
import { OrderInfo } from "@/entities/order";
export function OrderSummary({ orderId }: { orderId: string }) {
const order = OrderInfo.useOrder(orderId); // чистый FSD API
return (
<section>
<h2 className="text-lg font-semibold">Итоги заказа</h2>
<LegacyOrderSummaryAdapter
orderId={order.id}
onConfirm={() => OrderInfo.confirm(order.id)}
/>
</section>
);
}Правила миграции легаси:
- Легаси импортируется только из адаптера внутри слайса, дальше наружу выходит чистый API.
- Все побочные эффекты и костыли держим в
lib/legacy-*. - Добавьте тесты на адаптер, чтобы не зависеть от легаси-среды.
Сложная сущность Product с агрегированными данными
Дерево сущности product показывает, как хранить связанные типы.
entities/
product/
model/types.ts
model/selectors.ts
api/product-api.ts
lib/normalizers.ts
ui/product-card.tsx
index.ts
Типы Product/Variant/Seo описывают сложную модель.
// entities/product/model/types.ts
export type ProductImage = { id: string; url: string; alt?: string };
export type ProductVariant = {
id: string;
sku: string;
price: number;
currency: string;
inStock: boolean;
};
export type ProductSeoData = {
title: string;
description: string;
keywords: string[];
canonical: string;
};
export type Product = {
id: string;
title: string;
description?: string;
variants: ProductVariant[];
images: ProductImage[];
seo: ProductSeoData;
};fetchProduct агрегирует данные из нескольких эндпоинтов.
// entities/product/api/product-api.ts
import { apiClient } from "@/shared/api";
import type { Product } from "../model/types";
export async function fetchProduct(id: string): Promise<Product> {
const [core, seo, media] = await Promise.all([
apiClient.get(`/products/${id}`),
apiClient.get(`/products/${id}/seo`),
apiClient.get(`/products/${id}/media`),
]);
return {
...core.data,
seo: seo.data,
images: media.data.images,
variants: core.data.variants,
};
}useProduct хук проксирует серверный state через react-query.
// entities/product/model/use-product.ts
import { useQuery } from "@tanstack/react-query";
import { fetchProduct } from "../api/product-api";
export function useProduct(id: string, options?: { enabled?: boolean }) {
return useQuery({
queryKey: ["product", id],
queryFn: () => fetchProduct(id),
staleTime: 5 * 60 * 1000,
enabled: options?.enabled ?? Boolean(id),
});
}Почему так:
- Вся серверная стейт-логика живёт в
entities/product/apiиmodel. - Фичи и виджеты получают единый
useProductбез знания о трёх эндпоинтах. - При изменении бекенда переписываем только слой entities.
Тестирование с FSD: изоляция и интеграция
Мокаем снизу вверх: виджеты → фичи → entities → shared/api. Минимально мокаем инфраструктуру, максимум проверяем связки.
Unit-тесты: бизнес-логика и форматирование
Unit-тест formatUserName показывает проверку бизнес-утилиты.
// entities/user/lib/format-user-name.test.ts
import { formatUserName } from "./format-user-name";
describe("formatUserName", () => {
it("returns username fallback when no name", () => {
expect(
formatUserName({ username: "john", firstName: "", lastName: "" })
).toBe("john");
});
});Тест useLogin демонстрирует мок API и router.
// features/auth/login-form/model/use-login.test.ts
import { renderHook, act } from "@testing-library/react";
import { vi } from "vitest";
import { useLogin } from "./use-login";
vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));
vi.mock("@/shared/api", () => ({
authApi: { login: vi.fn().mockResolvedValue({ token: "t" }) },
}));
describe("useLogin", () => {
it("stores token and redirects", async () => {
const { result } = renderHook(() => useLogin());
await act(async () => {
await result.current.login({ email: "a@a", password: "123" });
});
expect(localStorage.getItem("auth_token")).toBe("t");
});
});Интеграционные тесты: связки слоёв
Интеграционный тест ProductGrid показывает связку виджет-фичи-entity.
// widgets/product-grid/__tests__/product-grid.integration.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ProductGrid } from "../ui/product-grid";
import { server } from "@/tests/msw/server";
import { http, HttpResponse } from "msw";
server.use(
http.get("/products", () =>
HttpResponse.json([{ id: "1", title: "MacBook Pro" }])
)
);
describe("ProductGrid", () => {
it("renders products from API", async () => {
const client = new QueryClient();
render(
<QueryClientProvider client={client}>
<ProductGrid />
</QueryClientProvider>
);
await waitFor(() => {
expect(screen.getByText("MacBook Pro")).toBeInTheDocument();
});
});
});Подход:
- Unit — мокаем только прямые зависимости слайса (API, роутер).
- Интеграционные — мокаем только нижний слой (
shared/apiили HTTP), всё остальное используем как в реальности. - Для легаси-адаптеров — пишем снапшот/контракт-тесты, чтобы поймать неожиданные изменения.
Серые зоны FSD и боевые решения
Утилиты с бизнес-логикой
- Если функция знает о домене — её место в
entities/{entity}/libи она попадает в Public API. - Пример:
entities/currency/lib/format-price.ts, экспортируется какformatPriceизentities/currency. - В
shared/libоставляем только чистые утилиты без доменной модели.
Общая аналитика и кросс-фичи
- Создайте отдельный слайс для ядра процесса:
entities/analyticsилиprocesses/analytics. - Экспортируйте чистые функции
trackEvent,trackError, которые не знают о UI. - Фичи вызывают аналитику напрямую, не импортируя друг друга, чтобы не нарушать изоляцию.
Глобальные UI-состояния (toast, modal, confirm)
- Провайдеры живут в
app/(ToastProvider,ModalProvider). - Хуки для диспатча — в
shared/lib/use-toast.ts,shared/lib/use-modal.ts. - Фичи и виджеты импортируют хуки из
shared/lib, не зная о конкретной реализации UI.
Пересечение фич
- Если фичи A и B используют общую бизнес-логику — вынесите её в entity (
entities/session,entities/permissions). - Если общая логика — процесс (например, онбординг) — заведите слайс
processes/onboardingи композиционируйте его воwidgets.
FSD: итоговый вердикт
Плюсы: изоляция модулей, чистые границы, облегчённое удаление/добавление фич, масштабирование команд и кода, предсказуемые импорты.
Минусы: больше boilerplate (папки, index.ts), высокий порог входа, необходимость строгих линтов и ревью, риск over-engineering на небольших проектах, нужно дисциплинированно поддерживать Public API.
Когда не использовать: короткие проекты (< 6 месяцев), команда 1–3 человека, быстрые прототипы и лендинги, проекты с сильно плавающими требованиями, когда скорость важнее долгосрочной модульности.
Альтернативы:
Grouping by File Type— быстро стартовать, но хаотичные связи растут экспоненциально.Grouping by Route(Next.js route groups) — хорошо для небольших маркетинговых сайтов с независимыми страницами.- Простая структура "по фичам" без слоёв — ок для небольших SPA, но требует дисциплины в зависимостях.
Заключение
FSD - это не просто папки. Это архитектурное мышление: изоляция, явные зависимости, композиция вместо наследования.
Ключевые выводы
- Слои = иерархия ответственности - каждый слой решает свою задачу
- Импорты только вниз - строгая направленность зависимостей
- Public API обязателен - barrel exports через index.ts
- Композиция > наследование - слоты, DI, props для гибкости
- Изоляция features - фичи не зависят друг от друга
- shared без бизнес-логики - только переиспользуемые примитивы
- ESLint - ваш друг - автоматизируйте проверки правил
Чек-лист для code review
- Импорты идут только вниз по слоям
- Нет deep imports (только через index.ts)
- shared не содержит бизнес-логики
- features не импортируют другие features
- entities содержат только модели и минимальный UI
- widgets композируют features и entities
- Нет циклических зависимостей
- Public API документирован в index.ts
- Код находится в правильном слое
- Сегменты (ui, model, api) используются корректно
Когда НЕ использовать FSD
Если ваш кейс из раздела «FSD: итоговый вердикт» (лендинг, прототип, маленькая команда) — используйте простую структуру и не тратьте время на слои:
Упрощённое дерево для маленьких проектов показывает альтернативу FSD.
src/
components/
utils/
pages/
Следующие шаги
- Практика - начните новый проект с FSD или мигрируйте существующий
- ESLint - настройте автоматические проверки правил
- Code Review - внедрите чек-лист в процесс ревью
- Документация - документируйте соглашения команды
- Обучение - проведите воркшоп для команды
Полезные ресурсы
Официальная документация:
- Feature-Sliced Design - официальный сайт
- FSD Examples - примеры проектов
- Awesome FSD - подборка материалов
Инструменты:
- Steiger - линтер для FSD
- @feature-sliced/eslint-config - готовый ESLint конфиг
Сообщество:
- FSD Telegram - русскоязычное комьюнити
- FSD Discord - международное сообщество
Статьи и доклады:
- Архитектура фронтенд-приложений на React. (Нам не нужен FSD) - еще один взгляд
- Архитектура Frontend проектов - доклад Ильи Азина
Помните: Архитектура - это не догма. Адаптируйте FSD под свой проект и команду. Главное - понимать принципы и следовать им осознанно.