Масштабируемость корпоративного фронтенд-приложения определяется архитектурной дисциплиной: уровнем связности компонентов (Coupling) и их зацепления (Cohesion). В тривиальных прототипах стандартная файловая структура Next.js «из коробки» (директории app/ или pages/) допустима. Однако по мере роста кодовой базы — при появлении сотен маршрутов, десятков бизнес-доменов, интеграций с внешними API и распределенного стейт-менеджмента — отсутствие строгих архитектурных барьеров приводит к образованию запутанных циклических зависимостей и спагетти-кода.
Организация кодовой базы по типу файлов (components/, hooks/, utils/, types/) катастрофически деградирует на дистанции: разработчику приходится открывать десятки разнородных каталогов для модификации одной формы оформления заказа. Современный стандарт разработки требует перехода к вертикальным срезам (Vertical Slices) и методологии Feature-Sliced Design (FSD). Бизнес-логика, пользовательский интерфейс, схемы валидации и мутации данных группируются вокруг предметной области, изолируя изменения и предотвращая регрессионные ошибки.
В архитектуре Next.js App Router проектирование усложняется гибридной моделью исполнения: дерево компонентов расщепляется на серверный рантайм (React Server Components, Node.js/Edge) и клиентский рантайм (DOM/Hydration). Архитектура обязана гарантировать, что приватные секреты, ORM-запросы и тяжелые серверные утилиты никогда не просочатся в публичный клиентский бандл через неконтролируемые реэкспорты.
Архитектура масштабируемого приложения на базе Next.js строится по принципу слабой связанности (low coupling) и высокого зацепления (high cohesion). Каждый вертикальный срез (feature slice) инкапсулирует в себе полный цикл бизнес-логики: изолированные UI-компоненты, слой мутации данных (Server Actions), строгую типизацию и локальное состояние.
В архитектуре FSD действует фундаментальное правило: Однонаправленный поток зависимостей (Unidirectional Dependency Flow). Верхние слои знают о нижних, но нижние слои изолированы от контекста верхних.
┌─────────────────────────────────────────────────────────┐
│ NEXT.JS ROUTER │
│ src/app/[slug]/page.tsx │
│ (Маршрутизация, await params, сборка композиции) │
└────────────────────────────┬────────────────────────────┘
│ imports ↓
┌────────────────────────────▼────────────────────────────┐
│ WIDGETS │
│ src/widgets/Header/ │
│ (Крупные самодостаточные блоки UI) │
└──────────────┬───────────────────────────┬──────────────┘
│ imports │ imports
┌──────────────▼──────────┐ ┌──────────────▼──────────────┐
│ FEATURES │ │ FEATURES │
│ src/features/auth/ │ │ src/features/checkout/ │
│ (Пользовательские шаги, │ │ (Server Actions, UI-формы) │
│ Zod-валидация) │ └──────────────┬──────────────┘
└──────────────┬──────────┘ │
НЕЛЬЗЯ │ × Запрещены импорты │
перекрестно │ между фичами! │ imports
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ ENTITIES │
│ src/entities/user/ src/entities/cart/ │
│ (Бизнес-сущности, схемы таблиц, типы, селекторы данных) │
└────────────────────────────┬────────────────────────────┘
│ imports ↓
┌────────────────────────────▼────────────────────────────┐
│ SHARED │
│ src/shared/ui (UI-Kit) src/shared/lib (Хелперы) │
│ src/shared/api (Клиенты) src/shared/types (База) │
└─────────────────────────────────────────────────────────┘Идентификация бизнес-сущностей (User, Order) и пользовательских фичей (Auth, Cart, Filter).
Вынос UI-Kit, конфигурации окружения и чистых утилит в src/shared/ без зависимостей от бизнес-логики.
Группировка схем данных, интерфейсов моделей и атомарных UI-карточек в src/entities/.
Инкапсуляция интерактивных форм, Server Actions и схем валидации Zod в изолированные срезы src/features/.
Объединение фичей и сущностей в крупные композиционные блоки интерфейса (Header, Sidebar, Feed).
Превращение страниц app/[route]/page.tsx в легковесный Glue Code с резолвом асинхронных params.
В крупном проекте на Next.js разработчик фичи оформления заказа (@/features/checkout) напрямую импортировал хук useCartStore из соседней фичи (@/features/cart). К чему это приведет и как исправить архитектурную ошибку согласно правилам FSD?
Colocation — фундаментальный принцип организации кодовой базы. Если компонент или утилита используются строго в пределах одного маршрута или одной фичи, они изолируются локально и не выносятся в глобальные разделы.
Конфигурация алиасов путей фиксирует архитектурные слои проекта на уровне TypeScript-компилятора, исключая хрупкие относительные пути вида ../../../../shared/ui.
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/app/*": ["./src/app/*"],
"@/widgets/*": ["./src/widgets/*"],
"@/features/*": ["./src/features/*"],
"@/entities/*": ["./src/entities/*"],
"@/shared/*": ["./src/shared/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}server-only#Для предотвращения случайного импорта серверного кода, секретов окружения или ORM-клиентов в клиентские компоненты используется пакет server-only.
pnpm add server-onlyimport 'server-only';
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const db =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = db;
}Если файл, содержащий import 'server-only', будет импортирован в файл с директивой 'use client', компилятор Next.js немедленно прервет сборку с подробной диагностической ошибкой.
Рабочая структура промышленного приложения объединяет изолированные слои внутри src/:
src/
├── app/ # Next.js App Router: инфраструктурный клей
│ ├── (auth)/ # Группа маршрутов без влияния на URL
│ │ ├── login/page.tsx
│ │ └── register/page.tsx
│ ├── (dashboard)/
│ │ ├── projects/
│ │ │ ├── [id]/page.tsx
│ │ │ └── _components/ # Private Folder: локальные компоненты страницы
│ │ │ └── ProjectHeader.tsx
│ │ └── layout.tsx
│ ├── layout.tsx # Корневой макет
│ └── globals.css
├── widgets/ # Композиционные блоки страниц
│ └── Navbar/
│ ├── ui/Navbar.tsx
│ └── index.ts # Public API виджета
├── features/ # Пользовательские бизнес-сценарии
│ ├── auth-by-email/
│ │ ├── ui/LoginForm.tsx # Клиентский UI с useActionState
│ │ ├── server/actions.ts # Server Actions с валидацией Zod
│ │ ├── model/schema.ts # Схемы валидации
│ │ └── index.ts
│ └── project-create/
├── entities/ # Бизнес-сущности домена
│ ├── user/
│ │ ├── ui/UserAvatar.tsx
│ │ ├── model/types.ts
│ │ └── index.ts
│ └── project/
└── shared/ # Базовый переиспользуемый код
├── api/ # Клиенты внешних сервисов
├── config/ # Типизированные env-конфигурации
├── lib/ # Адаптеры библиотек (db, logger)
├── ui/ # Dumb UI-Kit (Button, Input, Modal)
└── utils/ # Чистые функции без сайд-эффектовСтраница в src/app/ выступает композитором: она асинхронно резолвит параметры маршрута, запрашивает агрегированные данные и передает их в скомпонованные виджеты.
import { notFound } from 'next/navigation';
import { ProjectHeader } from './_components/ProjectHeader';
import { fetchProjectById } from '@/entities/project/server';
import { ProjectMembersWidget } from '@/widgets/ProjectMembersWidget';
interface ProjectPageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ tab?: string }>;
}
export default async function ProjectPage({ params, searchParams }: ProjectPageProps) {
const { id } = await params;
const { tab = 'overview' } = await searchParams;
const project = await fetchProjectById(id);
if (!project) {
notFound();
}
return (
<div className="flex flex-col gap-8 p-6 max-w-7xl mx-auto w-full">
<ProjectHeader project={project} currentTab={tab} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
{/* Специфичный контент таба */}
</div>
<div>
<ProjectMembersWidget projectId={project.id} />
</div>
</div>
</div>
);
}Смешивание утилит и сервисов приводит к размыванию ответственности. В архитектуре Fullstack Agentic Lab действует следующая градация:
shared/utils/ — Чистые детерминированные функции.
formatCurrency(amount, currency)).shared/lib/ — Адаптеры сторонних библиотек.
lib/).shared/services/ (или services внутри feature/entity) — Слой интеграций.
Команда создала файл src/features/auth/index.ts, реэкспортирующий как серверные функции (loginAction с доступом к Prisma и секретам), так и клиентскую форму (LoginForm с 'use client'). Что произойдет при импорте LoginForm в клиентском компоненте на странице личного кабинета?
Создание общего файла index.ts в корне фичи, который одновременно реэкспортирует серверные функции с доступом к базе данных и клиентские компоненты с директивой 'use client', разрушает изоляцию бандла. Если клиентский компонент другой фичи импортирует тип или кнопку из такого barrel-файла, компилятор рискует затянуть серверные зависимости в клиентский бандл либо выдать ошибку компиляции.
Решение: Разделяйте публичные контракты по контекстам выполнения:
@/features/auth/ui — клиентские компоненты формы.@/features/auth/server — Server Actions и доступ к БД с маркером import 'server-only'.@/features/auth/model — чистые схемы Zod и TypeScript-типы.Прямой импорт компонента или хука из @/features/cart напрямую в @/features/checkout. Это создает жесткую циклическую связность (circular dependency). При модификации интерфейса корзины неизбежно ломается процесс оформления заказа.
Решение: Если фиче checkout требуются данные корзины:
@/widgets/checkout-cart) или непосредственно в серверном компоненте страницы app/checkout/page.tsx.@/entities/cart.Оборачивание всего дерева RootLayout в массив контекстных провайдеров (ThemeProvider, QueryClientProvider, SessionProvider) прямо внутри серверного файла layout.tsx превращает поддерево страниц в зону риска гидратации и раздувает Initial JS Payload.
Решение: Создавайте отдельный изолированный провайдер-компонент @/shared/providers/AppProviders.tsx с директивой 'use client' и пропсом { children: React.ReactNode }. В RootLayout передавайте серверный JSX через композиционный паттерн children.
Разработчики часто создают папку app/(components)/Header.tsx, полагая, что файлы игнорируются маршрутизатором. Круглые скобки (group) исключают сегмент из URL, но не защищают от маршрутизации вложенные page.tsx! Если внутри (group) появится файл с зарезервированным именем, он станет доступным по непреднамеренному пути.
Решение: Для изоляции локальных компонентов страницы, хелперов и тестов внутри app/ используйте исключительно приватные папки с префиксом подчеркивания: app/dashboard/_components/ или app/dashboard/_utils/.