Старт проекта на Next.js
Фундамент дешевле всего заложить в первый день: потом каждое правило
приходится внедрять поверх кода, который его нарушает. Если есть шаблон
frontend-starter — начинай с копии шаблона: в нём шаги ниже уже сделаны и
проверены, а этот документ остаётся объяснением решений и чек-листом аудита.
Этот документ задаёт
порядок шагов и минимальный набор. Как писать сам код — в скилле
fsd-frontend (если он есть в проекте), авторизацию — в
frontend-auth-session, проверку перед коммитом — в anti-slop-review.
Готовые конфиги — в references/configs.md, раскладка Next + FSD с
примерами кода — в references/next-structure.md.
Приоритет правил. Явные инструкции пользователя и agent-файлы проекта
(AGENTS.md, CLAUDE.md) важнее этого документа. Версии библиотек
меняются: перед тем как копировать конфиг, сверь его с документацией
установленной версии и не утверждай, что он работает, пока не запустил.
Факты о Next.js — из документации в пакете, а не из этого документа и
не из памяти. С 16.2 документация лежит в node_modules/next/dist/docs/,
а AGENTS.md проекта на неё ссылается. При расхождении с примерами отсюда
права документация установленной версии.
Обозначения: MUST — нарушать нельзя; SHOULD — по умолчанию да, отступление требует причины.
1. Решения до кода
Каждое решение из таблицы записывается в README (раздел «Решения») или в ADR, с причиной. Отступление от значения по умолчанию без записанной причины — не делается.
| Вопрос | По умолчанию | Когда иначе |
|---|---|---|
| Next.js или SPA | Next.js, если нужен хотя бы один из: SSR/SEO, BFF (секреты, HttpOnly-cookie), route handlers | Внутреннее приложение за логином без SEO и без BFF → Vite + React Router: проще сборка и деплой |
| Роутер | App Router | Pages Router — только в существующем проекте |
| Монорепо | Нет, одно приложение | Второе приложение или пакет реально появился → pnpm workspaces; Nx/Turborepo — когда сборка или CI начинают тормозить |
| Multi-Zones, микрофронтенды | Нет | Независимые команды с независимыми релизами |
| Серверные данные | Server Components для начальной загрузки; TanStack Query для интерактивных клиентских данных и мутаций | Чисто клиентское приложение → только TanStack Query |
| Тексты и i18n | Модуль переводов с первого дня (next-intl или аналог), даже для одной локали, если проект следует fsd-frontend | Явное решение «без i18n» записано в README |
| Пакетный менеджер | pnpm, версия в packageManager | — |
| Деплой | Решить сразу: Vercel, Node-сервер (output: "standalone") или Docker | От этого зависят env, кэш, CI |
MUST: не добавлять технологию «на будущее». Монорепо, второй роутер, Storybook, стейт-менеджер, MSW «как тип бэкенда» появляются, когда у них есть первый реальный потребитель.
2. Порядок шагов
- Создать проект (
pnpm create next-app@latest): TypeScript, ESLint, Tailwind, App Router. ЗафиксироватьpackageManager,engines.node(Next 16 требует Node 20.9+) и.nvmrc. - Сразу удалить шаблон генератора: стартовую страницу, SVG из
public/, шаблонный README, демо-стили. Заменить README каркасом изreferences/configs.md. Шаблон, оставленный «на потом», доживает до продакшена.AGENTS.mdиCLAUDE.mdотcreate-next-appне удаляются: блок между<!-- BEGIN:nextjs-agent-rules -->и<!-- END:nextjs-agent-rules -->указывает агентам на документацию установленной версии,next devподдерживает его сам. Свои правила проекта дописываются вне этих маркеров. - Строгий TypeScript (раздел 4
references/configs.md) и скриптtypecheck(tsc --noEmit). - Структура FSD поверх Next (раздел 3 ниже).
- Линтер и форматирование: ESLint flat config от
eslint-config-next- type-aware правила + линтер границ + Prettier. Next 16 убрал
next lintи не линтит приnext build— линтер запускается отдельным скриптом и в CI.
- type-aware правила + линтер границ + Prettier. Next 16 убрал
- Env-конфиг (раздел 4).
- Модель ошибок и API-клиент в
shared/api(раздел 5). - Провайдеры в слое
_app: QueryClient, тема, i18n, глобальные сервисы (toast, confirm) как порт вshared+ реализация в_app. - Основа UI (раздел 6).
- Тесты: Vitest + Testing Library + MSW, Playwright для e2e (раздел 7).
- Git-хуки: husky + lint-staged (eslint --fix, prettier) + commitlint.
- CI: lint, typecheck, test, build на каждый push и PR (раздел 8).
- Безопасность по умолчанию (раздел 9).
- README заполнен тем, что уже правда (раздел 10), и Definition of Done пройден (раздел 11).
Шаги 1–3 — отдельный коммит «scaffold»: так в истории видно, что сделал генератор, а что ты.
3. FSD поверх роутинга Next
По официальному гайду FSD для App Router: служебная папка Next app/ —
в корне проекта, слои FSD — в src/, а слои app и pages получают
префикс _, чтобы не путаться со служебными папками Next.
app/ роутинг Next: только файлы маршрутов
├── layout.tsx корневой лейаут → провайдеры из src/_app
├── (auth)/sign-in/page.tsx export { SignInPage as default } from "@/_pages/sign-in"
└── api/auth/login/route.ts route handler (BFF), тонкий
proxy.ts Next 16+ (раньше middleware.ts), рядом с app/
src/
├── _app/ слой app: провайдеры, глобальные стили, реализации сервисов
├── _pages/ слой pages
├── widgets/ (если проект их использует)
├── features/
├── entities/
└── shared/{ui,api,lib,config}
MUST:
- Файлы в
app/тонкие: реэкспорт страницы изsrc/_pages,generateMetadata, разборparams/searchParams(в Next 15+ этоPromise, их надоawait) и передача пропсами. Логики в них нет. - Route handlers и Server Actions тонкие: разобрать и провалидировать вход →
вызвать функцию из
src→ вернуть ответ. Каждый из них — публичная точка входа: проверяет авторизацию сам, а не надеется наproxy.ts. - Не создавать
src/appиsrc/pages: Next ищет служебные папки и в корне, и вsrc, и папка с таким именем легко становится вторым роутером. - Линтер границ знает про префиксы: шаблоны
src/_app,src/_pages/*вместоsrc/app,src/pages/*(конфиг —fsd-frontend,references/conventions.md, раздел 11). "use client"ставится как можно ниже по дереву. Страница по умолчанию серверная, клиентские — листья с интерактивом.- Модули с секретами начинаются с
import "server-only", модули сwindow— сimport "client-only".
Подробно и с кодом: references/next-structure.md.
4. Конфигурация окружения
MUST:
- Переменные читаются в двух модулях:
shared/config/env.server.ts(import "server-only") иshared/config/env.client.ts. Схема —zod, валидация при импорте: ошибка конфигурации падает при старте, а не на первом запросе. В остальном кодеprocess.envзапрещён (правило линтера). NEXT_PUBLIC_*встраиваются в бандл при сборке и только при прямом обращенииprocess.env.NEXT_PUBLIC_X: без деструктуризации иprocess.env[name]. Один образ на несколько окружений с разными публичными значениями так не собрать — такие значения читаются на сервере и передаются вниз.- Секреты — только без префикса
NEXT_PUBLIC_. .env.exampleсодержит каждую переменную с комментарием и безопасным значением;.env*в.gitignore, кроме.env.example. Сборка в CI проходит на значениях из.env.example.
5. Модель ошибок и API-клиент
MUST:
- Ошибки нормализуются в
AppErrorсkindодин раз, на границеshared/api(модель —fsd-frontend,references/conventions.md, раздел 1). Вышеshared/apiникто не разбираетResponse, статусы и тексты ошибок. - Один
request()поверхfetch(или сгенерированный клиент, обёрнутый так же): пробрасываетsignal, переводит сетевую ошибку вnetwork, HTTP-статус — вkind, тело ответа валидирует схемой. Пример —references/next-structure.md. - Вид ошибки — по коду: статус,
extensions.code(GraphQL отвечает 200 и на ошибки),codeв теле. Если бэкенд отдаёт только текст — сопоставление по тексту в одном маппере, с комментарием-причиной и тестами. - Ответ бэкенда маппится в доменный тип на границе; DTO не уходят в UI.
- Данные пользователя не кэшируются между пользователями: никаких
"use cache"/unstable_cacheдля ответов, зависящих от сессии. - Клиент генерируется из схемы (OpenAPI, GraphQL), если схема есть. Схема или сгенерированный код лежат в репозитории: CI не должен ходить к живому бэкенду.
SHOULD: интерфейс над транспортом (AuthApi + фабрика) — только когда
у него уже есть второй потребитель (fsd-frontend,
references/state-and-data.md, 2.1). Значение конфига вида
type: "mock", которое бросает «not implemented», не заводится.
6. Основа UI
- Токены дизайна — CSS-переменные в
:root(и для тёмной темы), подключённые к Tailwind v4 через@theme. В компонентах — только токены; сырые HEX и произвольные значения (text-[#3a3a3a]) — разовое исключение с комментарием. - Классы существуют. Компоненты, скопированные из shadcn/ui и
примеров, приходят со своими токенами (
bg-card,text-muted-foreground). Либо заведи эти токены, либо замени классы — иначе компонент молча остаётся без стилей. - Примитивы
shared/uiстроятся на headless-библиотеке (Radix, React Aria) ради доступности; варианты —cva, склейка классов —cn(clsx+tailwind-merge). - Шрифты —
next/font, изображения —next/imageс размерами,images.remotePatternsперечислены явно. - Storybook — когда UI-кит переиспользуется несколькими приложениями или командами, не раньше.
7. Тесты
- Vitest + Testing Library + jsdom + MSW с
onUnhandledFrame: "error"(MSW 3; в MSW 2 —onUnhandledRequest). Конфиг —references/configs.md. - Vitest не умеет рендерить
asyncServer Components. Логику из них выноси в функции и тестируй unit-тестами, а саму страницу — в e2e. - Playwright для 3–10 критических путей. В CI e2e идут против предсказуемого бэкенда (мок-сервер, стенд с тестовой БД), а не против общего стенда, где тесты меняют чужие данные.
- Первый тест пишется в день ноль: он проверяет, что инфраструктура тестов работает, и задаёт образец.
8. CI
MUST: на каждый push в основную ветку и на каждый PR: lint,
typecheck, test, build. Шаблон GitHub Actions — references/configs.md.
Для публичных репозиториев Actions бесплатен, для приватных есть месячная
квота минут.
- Сборка в CI идёт на
.env.example, без секретов. - e2e — отдельной job, когда есть изолированный бэкенд; до тех пор — явная пометка в README, почему их нет в CI.
- Бейдж CI в README появляется вместе с workflow.
9. Безопасность по умолчанию
poweredByHeader: false; заголовки черезheaders()вnext.config.ts:X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin,X-Frame-Options: DENY(илиframe-ancestorsв CSP). CSP с nonce — отдельная задача, но решение «есть / нет и почему» записывается сразу.- Server Actions и route handlers — публичные эндпоинты: валидация входа схемой и проверка прав в каждом.
dangerouslySetInnerHTML— только с санитайзером и комментарием-причиной.- Персональные данные и токены не логируются.
- Авторизация — по
frontend-auth-session.
10. README
Каркас — references/configs.md. Правило одно: в README только то, что
правда для кода в этом коммите. Каждый раздел обновляется в том же
коммите, что и код, который он описывает. Планы — в отдельном разделе
«Дальше», с пометкой, что это не сделано.
11. Definition of Done для старта
- Шаблон генератора удалён; README — каркас, заполненный правдой.
-
packageManager,engines.node,.nvmrcзафиксированы. - TS strict;
typecheck,lint,test,buildпроходят локально (запущены, а не предположены). - FSD-раскладка с префиксами
_app/_pages; линтер границ ловит импорт вверх. - Env через два модуля со схемой;
process.envвне них запрещён линтером;.env.exampleполный. -
AppErrorиrequest()вshared/api, покрыты тестами (сеть, статусы, неверная схема). - Провайдеры в
_app; QueryClient не общий для пользователей на сервере. - Токены дизайна в CSS-переменных; все используемые классы существуют.
- Vitest + MSW + Playwright настроены, по одному живому тесту.
- Git-хуки и commitlint работают.
- CI зелёный.
- Заголовки безопасности и
poweredByHeader: false. - Решения из раздела 1 записаны.
12. Когда сомневаешься
- Между «добавить сейчас» и «добавить, когда понадобится» — когда понадобится.
- Между «настроить вручную» и «оставить как сгенерировал create-next-app» — оставь, пока нет причины менять, и запиши причину, когда меняешь.
- Между «одно приложение» и «монорепо» — одно приложение.
- Между «конфиг из памяти» и «конфиг из документации текущей версии» — из документации.