Масштабованість корпоративного фронтенд-застосунку визначається архітектурною дисципліною: рівнем зв'язності компонентів (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-запити до баз даних та важкі серверні утиліти ніколи не потраплять у клієнтський JavaScript-бандл через неконтрольовані реекспорти.
Архітектура масштабованого застосунку на базі 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/.
Консолідація схем даних, інтерфейсів моделей та пасивних карток відображення в src/entities/.
Упакування інтерактивних форм, Server Actions та Zod-схем в ізольовані зрізи src/features/.
Поєднання фічей та сутностей у комплексні самодостатні блоки сторінки (Header, Sidebar, ActivityFeed).
Перетворення app/[route]/page.tsx на легковаговий шар клею, що очікує параметри та збирає віджети.
У великому застосунку Next.js розробник фічі оформлення замовлення (@/features/checkout) напряму імпортував хук useCartStore із сусідньої фічі (@/features/cart). До чого це призведе та як виправити архітектурну помилку за правилами FSD?
Colocation є наріжним каменем організації стабільної кодової бази. Якщо компонент чи утиліта використовуються лише в межах одного маршруту або однієї фічі, вони ізолюються локально і не виносяться в глобальні каталоги.
tsconfig.json#Аліаси шляхів фіксують архітектурні шари проєкту на рівні компілятора 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)/ # Route Group (не впливає на 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 # Публічний API віджета
├── features/ # Інтерактивні користувацькі сценарії
│ ├── auth-by-email/
│ │ ├── ui/LoginForm.tsx # Клієнтська форма з useActionState
│ │ ├── server/actions.ts # Server Actions із валідацією Zod
│ │ ├── model/schema.ts # DTO-схеми валідації
│ │ └── index.ts
│ └── project-create/
├── entities/ # Бізнес-сутності домену
│ ├── user/
│ │ ├── ui/UserAvatar.tsx
│ │ ├── model/types.ts
│ │ └── index.ts
│ └── project/
└── shared/ # Базовий перевикористовуваний шар
├── api/ # Клієнти зовнішніх SDK
├── config/ # Типізовані змінні оточення
├── 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/ — Адаптери сторонніх бібліотек:
shared/services/ (або services усередині зрізів) — Шар інтеграцій:
Команда розробників створила файл src/features/auth/index.ts, який одночасно реекспортує Server Action (loginAction з прямим доступом до бази) та клієнтський компонент (LoginForm з директивою 'use client'). Що станеться, коли клієнтський компонент на сторінці кабінету імпортує LoginForm із цього barrel-файлу?
Створення загального index.ts, який водночас реекспортує Server Actions та клієнтські форми, руйнує ізоляцію бандлів. Якщо клієнтський компонент імпортує будь-яку сутність із такого barrel-файлу, компілятор затягує серверні модулі в браузер.
Рішення: Розділяйте точки входу за середовищем виконання:
@/features/auth/ui — клієнтські компоненти інтерфейсу.@/features/auth/server — Server Actions та запити до БД із захистом import 'server-only'.@/features/auth/model — чисті схеми Zod та TypeScript-типи.Прямий імпорт хука чи компонента з @/features/cart безпосередньо в @/features/checkout утворює циклічну зв'язність. Зміна кошика неминуче ламає флоу замовлення.
Рішення: Якщо фічі checkout потрібен стан кошика:
@/widgets/checkout-cart) або в серверному компоненті сторінки (app/checkout/page.tsx).@/entities/cart.Огортання всього дерева RootLayout у контекстні провайдери (ThemeProvider, QueryClientProvider) безпосередньо в app/layout.tsx перетворює всю сторінку на клієнтську та роздуває початковий JS-бандл.
Рішення: Створіть окремий клієнтський компонент @/shared/providers/AppProviders.tsx з директивою 'use client' і передавайте дочірні елементи через патерн композиції: {children}.
Розробники іноді створюють теку app/(components)/Header.tsx, вважаючи, що вона прихована від маршрутизатора. Хоча круглі дужки (group) виключають назву з URL, вони не блокують маршрутизацію для вкладених файлів page.tsx!
Рішення: Для надійного розміщення допоміжних компонентів та утиліт усередині app/ використовуйте виключно префікс підкреслення: app/dashboard/_components/.