Олег Марков
FSD для Next.js - nextjs-fsd архитектура фронтенда на фичах
Введение
Feature-Sliced Design (FSD) — это подход к архитектуре фронтенда, который строит проект вокруг фич, а не вокруг страниц или технических слоев. Он помогает держать кодовую базу управляемой, когда приложение растет, а команда расширяется.
В контексте Next.js часто возникает проблема: встроенная файловая архитектура (pages или app router) диктует структуру проекта, и со временем в папке app или pages накапливается множество несвязанных сущностей. Появляются «бесконечные» папки components, utils, «общие» хуки, которые используют все и везде, а связи между модулями становятся неочевидными.
Подход nextjs-fsd — это применение принципов FSD в рамках Next.js-проекта с учетом его особенностей:
- серверные и клиентские компоненты;
- роутинг через
appилиpages; - серверные действия (server actions);
- API-роуты и взаимодействие с backend.
Давайте разберемся, как вы можете шаг за шагом внедрить FSD в свой Next.js-проект, какие слои использовать, как раскладывать файлы, как организовать импорт, и посмотрим на конкретные примеры кода.
Базовые принципы FSD, адаптированные для Next.js
Слои иерархии
Стандартный FSD предлагает несколько слоев:
app— стартовая точка приложения, интеграция окружения;processes— сквозные бизнес-процессы (регистрация, онбординг, оплата);pages— композиция фич, виджетов и процессов в конкретный экран;widgets— крупные блоки интерфейса (header, sidebar, dashboard-widget);features— законченные пользовательские сценарии (логин, выбор товара, фильтрация списка);entities— бизнес-сущности (user, product, order);shared— общий фундамент (UI-кит, хелперы, конфиги).
Для Next.js важно понять, как это сочетается с его app или pages:
- Папка
src/app(илиapp) — это не FSD-слойapp, а системная часть Next.js, где живет роутинг. - FSD-слой
appудобно положить внутрьsrc, но отделить его отapp-роутера. Чуть ниже я покажу структуру.
Задача — не сломать встроенный роутинг, но при этом разложить бизнес-логику и компоненты по слоям FSD.
Основные принципы, которых стоит придерживаться
Фичи и сущности важнее страниц.
Страница — это композиция, а не место, где живет вся логика.Импорты «сверху вниз».
Верхние слои (pages, widgets) могут импортировать нижние (features, entities, shared), но не наоборот.Минимизировать «сквозняки».
Старайтесь не тащитьsharedвезде бездумно. Важнее строить зависимости вокруг сущностей и фич.Учитывать серверные/клиентские компоненты.
В Next.js 13+ (app router) каждый компонент по умолчанию серверный, а пометка'use client'делает его клиентским. В FSD-структуре это нужно отражать аккуратно, чтобы клиентские зависимости не «протекали» наверх.
Структура проекта Next.js с FSD (nextjs-fsd)
Пример структуры для Next.js 13+ c app router
Смотрите, я покажу вам один из типичных вариантов схемы директорий:
src/
app/ // Роутинг Next.js (app router)
layout.tsx
page.tsx
profile/
page.tsx
api/
auth/
route.ts // API route
appProviders/ // FSD-слой app (инициализация)
index.tsx
processes/
onboarding/
model/
ui/
pages/ // FSD-слой pages: композиция экранов
home/
ui/
HomePage.tsx
profile/
ui/
ProfilePage.tsx
widgets/
header/
ui/
Header.tsx
model/
sidebar/
ui/
Sidebar.tsx
features/
auth/
login/
ui/
LoginForm.tsx
model/
useLogin.ts
user/
updateProfile/
ui/
model/
entities/
user/
model/
types.ts
api.ts
selectors.ts
ui/
UserAvatar.tsx
UserProfileCard.tsx
shared/
ui/
Button/
Button.tsx
Input/
Input.tsx
api/
baseClient.ts
config/
env.ts
lib/
formatDate.ts
validators/
types/
global.d.ts
Здесь важно:
- папка
src/app— зона ответственности Next.js (роутер, layout, api routes); - FSD-слой
pagesсодержит реальную страницу с точки зрения бизнеса (HomePage,ProfilePageи т. д.); - Next.js-страницы в
src/appпросто подключают нужный FSD-экран.
Давайте разберем это на конкретном примере.
Связка Next.js-роута и FSD-страницы
Допустим, у нас есть главная страница /. В src/pages/home/ui/HomePage.tsx мы собираем экран:
// src/pages/home/ui/HomePage.tsx
import { Header } from '@/widgets/header/ui/Header'
import { Sidebar } from '@/widgets/sidebar/ui/Sidebar'
import { LoginForm } from '@/features/auth/login/ui/LoginForm'
import { UserProfileCard } from '@/entities/user/ui/UserProfileCard'
// Здесь мы собираем страницу из виджетов, фич и сущностей
export const HomePage = () => {
return (
<div className="layout">
<Header />
<div className="content">
<Sidebar />
<main>
<LoginForm />
<UserProfileCard />
</main>
</div>
</div>
)
}
Теперь вы увидите, как это связывается с Next.js-роутом:
// src/app/page.tsx
import { HomePage } from '@/pages/home/ui/HomePage'
// Это серверный компонент по умолчанию (Next.js 13+)
// Он просто рендерит FSD-страницу
export default function Page() {
return <HomePage />
}
Так мы разделяем:
app— отвечает за маршрутизацию;pages— отвечает за композицию интерфейса.
Слои FSD и их роль в Next.js-проекте
Слой app — интеграция приложения
В классическом FSD слой app — это точка входа, провайдеры, глобальные стили. В Next.js с app router глобальный layout и провайдеры часто живут в src/app/layout.tsx. Чтобы не смешивать инфраструктуру с бизнес-кодом, удобно вынести провайдеры в src/appProviders.
// src/appProviders/index.tsx
'use client'
import { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ThemeProvider } from 'next-themes'
const queryClient = new QueryClient()
// Здесь мы собираем все клиентские провайдеры
export const AppProviders = ({ children }: { children: ReactNode }) => {
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider attribute="class">
{children}
</ThemeProvider>
</QueryClientProvider>
)
}
Теперь подключаем это в layout:
// src/app/layout.tsx
import './globals.css'
import { AppProviders } from '@/appProviders'
// Layout по умолчанию серверный, но внутри мы можем использовать клиентские провайдеры
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<AppProviders>
{children}
</AppProviders>
</body>
</html>
)
}
Комментарии:
- layout остается максимально «тонким»;
- все, что связано с состоянием клиента, уходит в FSD-слой
appProviders(слойapp).
Слой pages — композиция экранов
Слой pages вы уже увидели на примере HomePage. Основная идея:
- не размещать бизнес-логику в Next.js-страницах;
- не смешивать routing и бизнесовый UI.
Еще один пример, теперь с профилем:
// src/pages/profile/ui/ProfilePage.tsx
import { Header } from '@/widgets/header/ui/Header'
import { UserProfileCard } from '@/entities/user/ui/UserProfileCard'
import { UpdateProfileForm } from '@/features/user/updateProfile/ui/UpdateProfileForm'
// Здесь мы строим страницу профиля из виджета и фичи обновления профиля
export const ProfilePage = () => {
return (
<>
<Header />
<section>
<UserProfileCard />
<UpdateProfileForm />
</section>
</>
)
}
И Next.js-роут:
// src/app/profile/page.tsx
import { ProfilePage } from '@/pages/profile/ui/ProfilePage'
// Next.js страница занимается только вызовом FSD-страницы
export default function Page() {
return <ProfilePage />
}
Слой widgets — крупные композиционные блоки
Виджеты — это готовые блоки, которые можно переиспользовать на разных страницах. Обычно они зависят от:
features;entities;shared.
Пример виджета Header:
// src/widgets/header/ui/Header.tsx
'use client'
import Link from 'next/link'
import { useUserAuth } from '@/features/auth/login/model/useUserAuth'
import { UserAvatar } from '@/entities/user/ui/UserAvatar'
import { Button } from '@/shared/ui/Button/Button'
// Здесь мы собираем шапку приложения с учетом авторизации
export const Header = () => {
const { user, logout } = useUserAuth()
return (
<header className="header">
<Link href="/">MyApp</Link>
<nav>
<Link href="/profile">Profile</Link>
</nav>
<div className="header-right">
{user ? (
<>
<UserAvatar user={user} />
<Button onClick={logout}>Logout</Button>
</>
) : (
<Link href="/login">
<Button>Login</Button>
</Link>
)}
</div>
</header>
)
}
Обратите внимание:
- Виджет помечен
'use client', потому что использует хукuseUserAuth. - Это осознанное решение — мы не делаем весь layout клиентским, только тот блок, который действительно требует клиентских фич.
Слой features — пользовательские сценарии
Фича — это «законченное действие» с точки зрения пользователя. Например: «логин», «добавление товара в корзину», «обновление профиля».
Давайте посмотрим на фичу auth/login.
Структура:
features/
auth/
login/
ui/
LoginForm.tsx
model/
useLogin.ts
types.ts
Код:
// src/features/auth/login/model/useLogin.ts
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { loginApi } from '@/entities/user/model/api'
// Этот хук инкапсулирует логику логина
export const useLogin = () => {
const router = useRouter()
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const login = async (email: string, password: string) => {
try {
setLoading(true)
setError(null)
// Вызов API логина, определенного на уровне сущности user
await loginApi({ email, password })
// После успешного логина можно сделать редирект
router.push('/profile')
} catch (e) {
setError('Login failed')
} finally {
setLoading(false)
}
}
return { login, loading, error }
}
Теперь UI-часть:
// src/features/auth/login/ui/LoginForm.tsx
'use client'
import { FormEvent, useState } from 'react'
import { useLogin } from '../model/useLogin'
import { Input } from '@/shared/ui/Input/Input'
import { Button } from '@/shared/ui/Button/Button'
// Эта форма использует хук useLogin и не знает деталей API
export const LoginForm = () => {
const { login, loading, error } = useLogin()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const onSubmit = async (e: FormEvent) => {
e.preventDefault()
await login(email, password)
}
return (
<form onSubmit={onSubmit}>
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
{error && <div className="error">{error}</div>}
<Button type="submit" disabled={loading}>
{loading ? 'Loading...' : 'Login'}
</Button>
</form>
)
}
Как видите, фича:
- знает о сущности
userтолько через API-слойentities/user; - ничего не знает о конкретной странице, на которой используется.
Слой entities — бизнес-сущности
Сущность — это центральный объект предметной области (user, product, order). В сущности концентрируется:
- типизация (types);
- API-доступ к данным;
- селекторы, адаптеры;
- базовые UI-компоненты, завязанные на конкретную сущность.
Пример для user:
// src/entities/user/model/types.ts
// Здесь мы описываем доменную модель пользователя
export type User = {
id: string
email: string
name: string
avatarUrl?: string
}
// src/entities/user/model/api.ts
import { User } from './types'
import { apiClient } from '@/shared/api/baseClient'
type LoginParams = {
email: string
password: string
}
// Этот модуль инкапсулирует работу с backend для сущности user
export const loginApi = async (params: LoginParams): Promise<User> => {
const res = await apiClient.post<User>('/auth/login', params)
return res.data
}
export const getCurrentUser = async (): Promise<User | null> => {
try {
const res = await apiClient.get<User>('/auth/me')
return res.data
} catch {
// Если пользователь не авторизован, возвращаем null
return null
}
}
// src/entities/user/ui/UserAvatar.tsx
import Image from 'next/image'
import { User } from '../model/types'
// Простой UI-компонент, показывающий аватар пользователя
export const UserAvatar = ({ user }: { user: User }) => {
return (
<div className="user-avatar">
<Image
src={user.avatarUrl || '/default-avatar.png'}
alt={user.name}
width={32}
height={32}
/>
</div>
)
}
Слой сущностей не должен знать о страницах и виджетах. Он предоставляет кирпичики, из которых собираются фичи и дальше — виджеты/страницы.
Слой shared — общая инфраструктура
Слой shared содержит то, что:
- не связано с конкретной сущностью;
- используется большим количеством слоев.
Он часто делится на:
ui— базовые UI-компоненты (Button, Input, Modal);lib— утилиты и хелперы;api— базовая настройка API-клиента;config— конфигурация.
Пример базового API-клиента, который пригодится для всех сущностей:
// src/shared/api/baseClient.ts
import axios from 'axios'
// Здесь мы инициализируем общий клиент для REST запросов
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
withCredentials: true,
})
Пример базовой кнопки:
// src/shared/ui/Button/Button.tsx
import { ButtonHTMLAttributes } from 'react'
import clsx from 'clsx'
type Props = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: 'primary' | 'secondary'
}
// Универсальная кнопка, не зависящая от конкретных фич
export const Button = ({ variant = 'primary', className, ...props }: Props) => {
return (
<button
className={clsx('btn', `btn-${variant}`, className)}
{...props}
/>
)
}
Работа с серверными и клиентскими компонентами в FSD
Next.js 13+ с app router вводит разделение:
- серверные компоненты (по умолчанию);
- клиентские компоненты (с директивой
'use client').
В архитектуре FSD для Next.js важно:
- четко понимать, какие слои преимущественно серверные/клиентские;
- не превращать все в клиентские компоненты без необходимости.
Рекомендации по слоям
Общая логика:
app(Next.js layout и страницы) — преимущественно серверные;- FSD-слой
pages— чаще серверный, но может включать клиентские компоненты; widgets— зависит от содержимого, часто есть и серверные, и клиентские варианты;features— часто клиентские (формы, интерактивность), но часть логики можно вынести в server actions;entities— смешанные, но UI часто использует клиентский код (состояние, хендлеры);shared— тоже смешанный, особенно UI.
Пример разделения: server actions + клиентская фича
Давайте разберем фичу обновления профиля, чтобы увидеть, как можно использовать server actions.
Сначала server action в Next.js-странице:
// src/app/profile/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { updateProfile } from '@/entities/user/model/serverApi'
// Эта серверная функция обновляет профиль и инвалидирует кеш страницы
export async function updateProfileAction(formData: FormData) {
const name = formData.get('name') as string
await updateProfile({ name })
// После обновления данных пересобираем страницу профиля
revalidatePath('/profile')
}
Теперь сущность user на серверной стороне:
// src/entities/user/model/serverApi.ts
import { cookies } from 'next/headers'
import { User } from './types'
// Этот модуль можно вызывать только на сервере
export const updateProfile = async (params: { name: string }): Promise<User> => {
const token = cookies().get('access_token')?.value
// Здесь мы делаем запрос к backend с использованием токена
// Комментарий - в реальном проекте используйте fetch/axios с учетом SSR
const res = await fetch(`${process.env.API_URL}/user/profile`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: token ? `Bearer ${token}` : '',
},
body: JSON.stringify(params),
})
if (!res.ok) {
throw new Error('Failed to update profile')
}
return res.json()
}
Теперь UI-фича, использующая server action:
// src/features/user/updateProfile/ui/UpdateProfileForm.tsx
'use client'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { updateProfileAction } from '@/app/profile/actions'
import { Button } from '@/shared/ui/Button/Button'
// Вспомогательный компонент, отслеживающий статус отправки формы
const SubmitButton = () => {
const { pending } = useFormStatus()
return (
<Button type="submit" disabled={pending}>
{pending ? 'Saving...' : 'Save'}
</Button>
)
}
// Клиентский компонент формы, который вызывает серверное действие
export const UpdateProfileForm = () => {
return (
<form action={updateProfileAction}>
<input name="name" placeholder="Your name" />
<SubmitButton />
</form>
)
}
Обратите внимание:
- фича остается клиентской, потому что использует
useFormStatusи интерактивную форму; - бизнес-логика обновления данных и взаимодействие с cookies сосредоточены в server action и server-side API в сущности
user.
Такой подход помогает:
- не тащить доступ к cookies и другие серверные детали в клиентские фичи;
- сохранить чистоту слоев в FSD.
Организация импортов и алиасов в nextjs-fsd
Чтобы FSD по-настоящему заработал, важно настроить алиасы, иначе вы быстро «утонете» в относительных путях.
Настройка алиасов в Next.js
В Next.js (с TypeScript) обычно используется tsconfig.json и next.config.js (или jsconfig.json для JS).
Простой пример tsconfig.json:
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["*"],
"@app/*": ["app/*"],
"@pages/*": ["pages/*"],
"@widgets/*": ["widgets/*"],
"@features/*": ["features/*"],
"@entities/*": ["entities/*"],
"@shared/*": ["shared/*"]
}
}
}
Комментарий:
baseUrlуказывает, что корнем для импортов является папкаsrc;- алиасы отражают FSD-слои, что сразу видно при чтении кода.
Теперь импорт выглядел бы так:
import { Header } from '@widgets/header/ui/Header'
import { LoginForm } from '@features/auth/login/ui/LoginForm'
import { User } from '@entities/user/model/types'
import { Button } from '@shared/ui/Button/Button'
Это делает связи между слоями визуально прозрачными.
Пошаговое внедрение FSD в существующий Next.js-проект
Если у вас уже есть проект с типичной структурой:
components/
pages/
lib/
и вы хотите перейти к nextjs-fsd, вы можете сделать это постепенно.
Шаг 1. Выделить shared-слой
Давайте начнем с простого:
- Создайте папку
src/shared. - Переместите туда:
- базовые UI-компоненты;
- утилиты (
lib); - конфигурацию.
Пример:
components/Button.tsx→shared/ui/Button/Button.tsx;lib/formatDate.ts→shared/lib/formatDate.ts.
Шаг 2. Выделить сущности (entities)
Посмотрите на свой код и найдите повторяющиеся «темы»:
- user;
- product;
- order;
- article.
Создайте src/entities/<entity> и начните переносить:
- типы;
- API-запросы;
- «карточки» и прочие UI-компоненты, которые явно завязаны на эту сущность.
Например:
components/UserCard.tsx→entities/user/ui/UserProfileCard.tsx;api/user.ts→entities/user/model/api.ts.
Шаг 3. Формирование фич (features)
Теперь определите пользовательские сценарии:
- логин;
- регистрация;
- фильтрация списка;
- добавление в корзину.
Создайте для них фичи:
features/auth/login;features/cart/addToCart.
Перенесите:
- формы;
- хуки, которые управляют этими формами;
- бизнес-правила, относящиеся к этим сценариям.
Шаг 4. Соберите виджеты (widgets)
Посмотрите на заголовки, боковые панели, карточки дашборда, которые состоят из нескольких фич и сущностей.
Создайте виджеты, например:
widgets/header;widgets/sidebar;widgets/productList.
Переместите туда сложные композиционные компоненты.
Шаг 5. Создайте слой pages
Для каждой важной страницы создайте FSD-страницу:
pages/home;pages/profile;pages/productDetails.
И перенесите туда сборку из виджетов и фич.
После этого Next.js-страницы (app/<route>/page.tsx или pages/*.tsx) станут тонкими обертками.
Практический пример: страница профиля пользователя в стиле nextjs-fsd
Давайте соберем все воедино на примере типичной страницы профиля.
1. Сущность user
// src/entities/user/model/types.ts
export type User = {
id: string
name: string
email: string
avatarUrl?: string
}
// src/entities/user/model/api.ts
import { User } from './types'
import { apiClient } from '@shared/api/baseClient'
// Получение профиля текущего пользователя
export const fetchProfile = async (): Promise<User> => {
const res = await apiClient.get<User>('/user/profile')
return res.data
}
// src/entities/user/ui/UserProfileCard.tsx
'use client'
import { useEffect, useState } from 'react'
import { fetchProfile } from '../model/api'
import { User } from '../model/types'
// Компонент, показывающий профиль пользователя
export const UserProfileCard = () => {
const [user, setUser] = useState<User | null>(null)
useEffect(() => {
// Здесь мы загружаем данные профиля при монтировании компонента
fetchProfile().then(setUser).catch(() => setUser(null))
}, [])
if (!user) {
return <div>Loading profile...</div>
}
return (
<div className="profile-card">
<h2>{user.name}</h2>
<div>{user.email}</div>
</div>
)
}
2. Фича обновления профиля
// src/features/user/updateProfile/ui/UpdateProfileForm.tsx
'use client'
import { FormEvent, useState } from 'react'
import { Button } from '@shared/ui/Button/Button'
import { Input } from '@shared/ui/Input/Input'
import { apiClient } from '@shared/api/baseClient'
// Форма, которая обновляет имя пользователя
export const UpdateProfileForm = () => {
const [name, setName] = useState('')
const [loading, setLoading] = useState(false)
const onSubmit = async (e: FormEvent) => {
e.preventDefault()
try {
setLoading(true)
// Отправляем новое имя на сервер
await apiClient.patch('/user/profile', { name })
// В реальном приложении можно еще обновить локальное состояние пользователя
} finally {
setLoading(false)
}
}
return (
<form onSubmit={onSubmit}>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="New name"
/>
<Button type="submit" disabled={loading}>
{loading ? 'Saving...' : 'Save'}
</Button>
</form>
)
}
3. Виджет профиля
// src/widgets/profile/ui/ProfileWidget.tsx
import { UserProfileCard } from '@entities/user/ui/UserProfileCard'
import { UpdateProfileForm } from '@features/user/updateProfile/ui/UpdateProfileForm'
// Виджет, который комбинирует данные профиля и форму редактирования
export const ProfileWidget = () => {
return (
<section>
<UserProfileCard />
<UpdateProfileForm />
</section>
)
}
4. FSD-страница профиля
// src/pages/profile/ui/ProfilePage.tsx
import { Header } from '@widgets/header/ui/Header'
import { ProfileWidget } from '@widgets/profile/ui/ProfileWidget'
// Страница, которая собирает header и виджет профиля
export const ProfilePage = () => {
return (
<>
<Header />
<main>
<ProfileWidget />
</main>
</>
)
}
5. Next.js-роут
// src/app/profile/page.tsx
import { ProfilePage } from '@pages/profile/ui/ProfilePage'
// Страница Next.js, которая просто рендерит FSD-страницу
export default function Page() {
return <ProfilePage />
}
Давайте посмотрим, что здесь важно:
- каждая часть системы знает только то, что ей нужно знать;
- логика API и типы сосредоточены в сущности
user; - фича
updateProfileрешает конкретную задачу и не тянет в себя лишнее; - виджет профиля собирает сущность и фичу в один блок;
- страница профиля собирает layout из виджета и заголовка.
Заключение
Подход nextjs-fsd позволяет согласовать две важные вещи:
- встроенную файловую архитектуру Next.js;
- бизнес-ориентированное разделение кода по фичам и сущностям.
Если кратко об основных идеях:
- Next.js-роуты в
appилиpages— транспортный уровень, они не должны содержать сложную бизнес-логику; - FSD-слои (app, pages, widgets, features, entities, shared) отвечают за структуру и связи внутри фронтенд-приложения;
- импорт по алиасам и движение зависимостей «сверху вниз» помогают избежать хаотичных связей;
- разделение на серверные и клиентские компоненты нужно проектировать осознанно, особенно в layers
featuresиentities; - внедрять FSD можно поэтапно, начиная с
shared, затемentities,features,widgetsи только потомpages.
Используя этот подход, вы получаете:
- предсказуемую структуру проекта;
- понятные границы между модулями;
- возможность масштабировать команду и функциональность без постоянных «рефакторингов всего на свете».
Частозадаваемые технические вопросы по теме и ответы
Как лучше организовать тесты в nextjs-fsd структуре
Рекомендуется сохранять ту же иерархию слоев. Вы можете использовать два подхода
1) Тесты рядом с кодомfeatures/auth/login/ui/LoginForm.test.tsxentities/user/model/api.test.ts
2) Отдельное дерево tests с зеркальной структуройtests/features/auth/login/LoginForm.test.tsx
Главное — не смешивать тесты разных слоев и сохранять тот же принцип зависимостей сверху вниз.
Как ограничить неправильные импорты между слоями
Вы можете использовать ESLint c плагином boundaries или import и задать правила
- запрещаем импорт
pagesизfeaturesиentities - разрешаем импорт
sharedотовсюду
Пример правилаno-restricted-importsс паттернами@pages/*для слоевfeaturesиentities.
Как хранить глобальное состояние в FSD при использовании Redux или Zustand
Создайте модуль состояния в shared или на уровне конкретной сущности
- глобальное состояние темы или языка —
shared/model/themeStore - состояние пользователя —
entities/user/model/userStore
Фичи работают с состоянием через публичные методы сущности или shared-стора, а не напрямую через внутреннюю реализацию.
Как поступать с layout компонентами Next.js в контексте FSD
Глобальные layout’ы и template-компоненты оставляйте в src/app. Внутри них можно использовать FSD-компоненты верхних слоев
например, Header из widgets. Избегайте логики фич и сущностей прямо в layout’ах — лучше подключайте виджеты и страницы FSD.
Как версионировать и переиспользовать фичи между несколькими Next.js приложениями
Если фича действительно общая
- вынесите ее в отдельный npm-пакет с той же FSD-структурой внутри пакета
- используйте алиасы вида
@acme/features/*
Следите, чтобы пакет не тянул в себя детали конкретного приложения — толькоsharedи общие сущности, которые вы готовы разделять между проектами.