07 февр. 2026 г.·6 мин чтения

Структура папок Next.js, которая выдержит через шесть месяцев

Структура папок Next.js быстро портится по мере роста приложения. Узнайте простой способ отделить маршруты, фичи и общий код до того, как начнётся хаос.

Структура папок Next.js, которая выдержит через шесть месяцев

Почему дерево расползается после шести месяцев

Проект редко ломается в один драматичный момент. Это происходит через множество небольших поспешных решений. Команда добавляет пару экранов, пару обработчиков API, новую форму и два эксперимента в одном спринте. Никто не планирует создать путаное дерево. Люди просто продолжают выпускать фичи.

В первый месяц структура всё ещё кажется простой. Один человек помнит, где что лежит. К шестому месяцу у продукта появляются состояния загрузки, файлы ошибок, server actions, схемы форм, тесты, хук аналитики и единичные помощники, которые появились под давлением дедлайна.

Большинство команд кладут новый код рядом с маршрутом, к которому последний раз прикасались. Это кажется быстрым и иногда действительно так. Если кто-то правит app/dashboard/page.tsx, он часто бросает модалку, конфиг таблицы, валидацию и вспомогательную функцию прямо рядом, потому что папка уже открыта.

Эта хитрость накапливается. Папка маршрута стартует как страница и превращается в склад. Теперь там лежат UI‑части, бизнес‑правила, логика fetch, типы и код, который нужен ещё одному экрану.

Потом начинается копирование. Команда делает фильтр‑бар для одной области, затем чуть другой — для другой, потом ещё один для админки. Один и тот же паттерн живёт в трёх местах с тремя названиями.

Импорты обычно подсказывают, когда структура дрейфует. Пути становятся длиннее и страннее. Файлы переезжают в shared, common или utils, потому что никто не знает, куда ещё их поместить. Эти папки часто оказываются просто мешками с яркими ярлыками.

Владение тоже стирается. Если файл лежит под маршрутом, значит ли, что этот маршрут его владеет? Если три экрана используют файл, стоит ли его переместить? Обычно эти вопросы задают слишком поздно — после того, как кто‑то изменил компонент и сломал другую часть приложения.

App Router может ухудшить ситуацию, потому что папки маршрутов смешивают фреймворковые файлы с продуктовым кодом. Это удобно на раннем этапе, но структура маршрутов и структура фич — разные вещи.

Здоровая структура Next.js обычно ломается по простой причине: команда растёт быстрее, чем правила размещения кода. Каждый новый файл кажется безобидным сам по себе, но дерево становится всё менее надёжным с каждой неделей.

Что должно быть на верхнем уровне

Корень проекта должен оставаться скучным. Если туда попадает каждый новый файл, структура быстро разваливается и никто не знает, куда смотреть через пару месяцев.

Оставьте верхний уровень ограниченным папками, которые отвечают на простые вопросы: где начинаются маршруты? Где живёт код фич? Что действительно разделено между областями? Что запускает проект вне самого приложения?

Обычно такая расстановка выдерживает нагрузку:

  • app для маршрутов, лэйаутов, шаблонов, состояний загрузки и точек входа страниц
  • features для продуктовых областей, таких как auth, billing, onboarding или dashboard
  • shared для кода, которым уже пользуются несколько областей
  • scripts, файлы конфигурации и глобальные файлы типов рядом с корнем

Это разделение держит каждую папку честной. Папка app должна описывать структуру URL и связывать страницы, но не хранить бизнес‑логику. Если странице биллинга нужны правила, формы, API‑вызовы и тесты, эти части должны лежать в features/billing, а не рядом с файлом маршрута.

Папка features работает лучше, когда каждая фича ощущается как маленький кусок продукта. Новый разработчик должен открыть features/auth и найти UI аутентификации, actions, хуки и тесты в одном месте. Это экономит время и сокращает классическую охоту по components, utils, hooks и services.

Для shared нужен самый строгий правило. Класть туда что‑то можно только после того, как этот код действительно используют две‑три части продукта. Кнопка, форматтер дат или обёртка API‑клиента могут туда подойти. Хелпер, используемый только биллингом, должен оставаться в billing.

Держите проектные файлы вне фич, потому что они не принадлежат одной продуктовой области. Это включает конфиг линтера, TypeScript, окружения, скрипты сборки и файлы вроде global.d.ts. На больших командах это особенно важно: люди работают быстрее, когда корень небольшой и каждая папка выполняет одну работу.

Как разделять маршруты и фичи

Если папка маршрута начинает собирать логику форм, получение данных, валидацию и вспомогательные файлы, она перестаёт быть картой URL. Это превращается во второе приложение, спрятанное в app, и именно там обычно начинается беспорядок.

Держите app сфокусированным на навигации. Маршрут должен говорить Next.js, какой URL существует, какой лэйаут его оборачивает и какую точку входа фичи рендерить. Продуктовые правила должны жить в другом месте.

Простой тест: если код всё ещё имеет смысл после смены URL — он не принадлежит app. Форма регистрации, калькулятор цен, запрос инвойса или проверка прав — часть фичи, а не часть /signup или /billing.

Чёткое разделение простое: app держит страницы, лэйауты, состояния загрузки и обработчики маршрутов. features содержит области продукта: auth, billing, onboarding или отчёты. Каждая фича хранит свои компоненты, схемы, server actions, запросы и тесты вместе.

Это делает изменения локальными. Если вы переименуете /settings/billing в /account/billing, вы переместите тонкий маршрутный файл вместо того, чтобы тащить за собой всю логику.

Каждая папка фичи должна чувствоваться завершённой. features/billing может содержать BillingForm.tsx, billing.schema.ts, billing.actions.ts, billing.queries.ts и billing.test.ts. Тогда app/account/billing/page.tsx может импортировать BillingPage из этой папки и оставаться скучным. Скучные файлы маршрутов — хороший знак.

Команды часто избегают такого разделения, потому что кажется, что это лишняя структура. Через шесть месяцев она экономит время. Новые люди быстро сканируют дерево, и меньше файлов попадает в случайные папки маршрутов, потому что для них уже есть очевидный дом.

Не превращайте features в второй ящик для хлама. Если код специфичен для billing, держите его в billing, даже если пару маршрутов его используют. Переносите в shared только когда код действительно общий и по‑прежнему имеет смысл без контекста продукта.

Маршруты описывают URL. Фичи содержат работу. Это простое правило и оно работает долго.

Что должен и не должен содержать shared

Shared код тоже должен оставаться скучным. Если файл несёт продуктовые правила, поведение, завязанное на странице, или имена бизнеса — он не для shared.

Без этого правила shared превращается в ящик, куда прячут старый код.

Что входит в shared

Файл заслуживает места в shared только если проходят несколько проверок: его уже использует более одной фичи; он не содержит продуктовых слов; он не импортирует из папки фичи; его можно было бы переместить в другой проект с минимальными изменениями.

Для UI держите shared/ui маленьким и простым. Кнопки, модалки, поля формы, аватары, табы и примитивы лэйаута туда подходят. BillingPlanCard — нет. Даже если две страницы биллинга её используют, смысл всё равно продуктовый, и компонент принадлежит billing.

То же правило работает для хукoв. Поместите общие хуки в shared/hooks, например useDebounce, useMediaQuery или useLocalStorage. Хуки вроде useCheckoutFlow, useWorkspaceMembers или useProjectLimits возвращайте в папки фич — они знают слишком много о своей части приложения.

Простой тест: попробуйте переименовать файл, убрав бизнес‑слова. Если имя становится расплывчатым или глупым, скорее всего это не shared.

Что shared должен отвергать

Папки вроде helpers, utils и common обычно породили хаос. Они звучат безвредно, но не говорят, что туда положено. Скоро вы получите date.ts, format.ts, api.ts, misc.ts и пару полуиспользуемых хуков, к которым никто не хочет прикасаться.

Выбирайте папки по назначению. Используйте имена вроде shared/ui, shared/hooks, shared/lib и shared/config. Затем держите каждую папку строгой. shared/lib может содержать небольшие чистые функции, но не математику биллинга, привязанную к вашей модели цен.

На стартапе это особенно важно. Вы можете двигаться быстро без хаоса, если продуктовая логика остаётся внутри фич, а shared — по‑настоящему общим. Когда кто‑то кладёт useInvoicesTableState в shared/hooks, верните его обратно в features/invoices прежде, чем вокруг него соберутся ещё три файла.

Пошаговый план очистки

Сделайте файлы маршрутов тонкими
Oleg поможет вынести бизнес-логику из app без полного рефактора.

Начните с маршрутов, которыми пользуются чаще всего. Они показывают проблему быстрее, потому что тянут код из многих мест. Откройте два‑три загруженных экрана, проследите каждый компонент, хук, хелпер, схему и server action, которые они импортируют, и запишите список. Короткий аудит обычно показывает, где структура начала дрейфовать.

Затем рассортируйте эти файлы по продуктовым областям, а не по типу файла. Если страница биллинга использует форму, валидатор, server action и пару UI‑частей, эта работа принадлежит биллингу. Разделение всего на components, hooks, utils и lib сначала кажется аккуратным, но одно изменение разбрасывает правки по всему репозиторию.

Используйте узкий цикл очистки:

  1. Выберите один кластер маршрутов, например app/dashboard/billing.
  2. Создайте одну папку фичи для этой области.
  3. Перемещайте связанные файлы небольшими пакетами.
  4. Поправляйте импорты после каждой пачки и сразу запускайте приложение.

Держите объём маленьким. Если переместить пять областей одновременно, получится огромный diff, сломанные пути и ревью, к которому никто не захочет прикоснуться. Одна фича за раз кажется медленнее первый день, но быстрее следующие шесть месяцев.

Когда вы сомневаетесь, используйте простой тест. Если файл сейчас использует только одна продуктовая область, держите его внутри этой фичи. Если несколько областей используют его, проверьте, действительно ли они разделяют одно и то же правило или просто похожие вещи. Команды часто толкают почти‑готовый код в shared преждевременно, а потом месяцами обходятся вокруг него.

После первого перемещения запишите несколько правил по папкам, прежде чем появится новый код. Держите их короткими, чтобы люди действительно прочитали: маршруты описывают URL, фичи содержат код одной продуктовой области, shared — стабильный код, используемый несколькими областями, и новые файлы начинаются рядом с фичей, которая в них нуждается.

Это не требует большой архитектурной встречи. Небольшая команда может очищать по одной фиче в неделю и остановить рост misc. Растущие продукты обычно восстанавливаются через спокойные последовательные перемещения, а не через героическое переписывание.

Простой пример из растущего продукта

Панель управления часто начинается с малого. В первый месяц есть страница пользователей, пара форм и файл маршрута, который делает слишком многое. К шестому месяцу продукт добавляет биллинг и настройки, и старые папки components, lib и utils начинают наполняться несвязанным кодом.

Предположим, приложение имеет /dashboard/users, затем добавляет /dashboard/billing и /dashboard/settings. Ошибка — продолжать закидывать код биллинга в общие места только потому, что он кажется повторно используемым. Биллинг обычно требует собственной таблицы, формы платежа, правил ввода и server actions, поэтому он должен жить как одна фича.

app/
  dashboard/
    users/page.tsx
    billing/page.tsx
    settings/page.tsx

features/
  billing/
    components/
      billing-table.tsx
      payment-form.tsx
    lib/
      validators.ts
      actions.ts
  users/
    components/
      users-table.tsx

shared/
  ui/
    button.tsx
    modal.tsx
  lib/
    currency.ts

С такой настройкой app/dashboard/billing/page.tsx остаётся коротким. Он читает параметры маршрута, проверяет доступ при необходимости и рендерит фичу billing. Файл страницы не владеет разметкой таблицы, правилами формы или server action, который обновляет план.

Папка shared остаётся узкой. Положите туда кнопку, потому что её может использовать каждая часть приложения. Положите модалку по той же причине. Форматтер валют подходит, если пользователи, биллинг и отчёты все нуждаются в одинаковом выводе. Не переносите billing-table.tsx или payment-form.tsx в shared только потому, что другой экран биллинга может их переиспользовать. Они всё ещё принадлежат billing.

Здесь структура начинает казаться удобнее. Новая работа имеет очевидный дом. Когда биллинг растёт, команда добавляет файлы внутрь features/billing, а не сбрасывает их в components или lib, надеясь, что имена останутся ясными.

Ошибки, которые восстанавливают папку с мусором

Проверьте структуру репозитория
Пусть Oleg заметит слабые границы папок до того, как они замедлят команду.

Запутанное дерево редко появляется из‑за одного большого промаха. Оно растёт от маленьких сокращений, которые кажутся безобидными неделю, но раздражают через полгода.

Одна распространённая проблема — случайные server actions. Команда добавляет action внутри папки маршрута, потому что страница нуждается в нём сейчас, затем добавляет другой action для другого потока завтра. Вскоре app хранит бизнес‑логику, обработку форм, валидацию и файлы маршрутов в одной связке. Если action принадлежит billing, auth или onboarding, держите его в соответствующей фиче и пусть файл маршрута остаётся тонким.

Имена тоже создают проблемы. Папки misc, temp, helpers или utils2 — это ящики для хлама с новым брендингом. Они не говорят, что туда помещать, поэтому люди продолжают туда кидать файлы. Хорошее имя само подсказывает решение. Если папку нельзя назвать по назначению, код, вероятно, принадлежит более конкретному месту.

Ещё один лёгкий путь восстановить беспорядок — переносить только половину фичи. Кто‑то начинает чистку и переносит UI‑компоненты в features/billing, но оставляет хуки в старой папке маршрута и типы в shared, потому что их перенос кажется необязательным. Через месяц billing живёт в трёх местах. Следующий человек копирует файл вместо того, чтобы искать все ссылки по проекту.

Я предпочту один слегка несовершенный перенос, чем наполовину начатую чистку. Когда перемещаете фичу, переместите компоненты, actions, тесты, типы и хелперы вместе, если нет ясной причины поступать иначе.

Слишком ранний перенос в shared создаёт тихую версию той же проблемы. Хелпер попадает в shared после одного реюза, потому что «может пригодиться позже». Чаще всего его больше не переиспользуют. Тогда shared превращается в склад для кода, который принадлежит одной фиче, но потерял дом.

Простое правило: держите код внутри фичи до тех пор, пока вторая фича действительно не начнёт его использовать; переименуйте расплывчатые папки прежде, чем они наполнятся; завершайте переносы за один проход; и рассматривайте папки маршрутов как точки входа, а не хранилища.

Быстрые проверки перед добавлением файла

Получите помощь fractional CTO
Используйте опыт, чтобы разделить маршруты, фичи и общий код понятными правилами.

Пауза в 20 секунд перед созданием нового файла экономит часы впоследствии, потому что многие запутанные деревья начинаются с одной безобидной хитрости, которую вся команда повторяет.

Начните с переиспользования. Если файл нужен только одному маршруту сейчас и, вероятно, будет ему принадлежать в будущем, держите его рядом с этим маршрутом. Форма оформления заказа, используемая только в биллинге, должна жить с биллингом. Если переместить её в shared преждевременно, люди потратят время на её поиск и в итоге скопируют обратно.

Затем оцените, что именно делает файл. Общие вещи — в shared. Продуктовые правила — в фиче. Форматтер дат, debounce или простой обёртчик ввода — для shared. Функция, решающая, может ли пользователь обновиться, увидеть триал или открыть отчёт — в фиче, потому что она объясняет сам продукт.

Названия папок проходят тот же тест. Если имя сейчас кажется расплывчатым, оно будет стареть плохо. utils, misc, common и helpers кажутся безобидными неделю; после шести месяцев они превращаются в ящики для хлама.

Структура должна делать размещение скучным. Новый человек должен угадать место файла за минуту. Если нужно спрашивать в Slack, листать шесть папок или открывать случайные файлы — структура уже провалилась.

Простой интуитивный тест: держите код маршрута с маршрутом, продуктовую логику — с фичей, в shared только по‑настоящему общий код, и переименуйте расплывчатые папки прежде, чем они соберут больше файлов.

Небольшой пример: PlanCard, используемый на pricing, onboarding и account, может находиться в shared/ui. Правило canUpgradePlan() — нет. Даже если три страницы вызывают его, правило остаётся в billing или subscriptions, а не в общей папке хелперов.

Команды часто усложняют. Вам не нужно идеальное место навсегда. Нужно место, которое всё ещё будет иметь смысл через три месяца. Если двое коллег положили бы файл в две разные папки, исправьте имена папок прежде чем коммитить.

Что делать дальше с командой

Чистая структура не выживает на хороших намерениях. Она выживает, когда команда использует одни и те же простые правила каждую неделю, особенно в условиях дедлайнов.

Начните с одностраничной карты папок. Держите её короткой, чтобы новый разработчик мог прочитать за две минуты. Покажите основные папки, для чего каждая нужна, и пару реальных примеров: где лежит файл маршрута, где компонент фичи, и где заканчиваются общие хелперы.

Набор коротких правил обычно и есть всё, что нужно. Кладите файлы маршрутов в app и держите их тонкими. Кладите код фичи рядом с фичей, а не в общий utils или components. Переносите в shared только после того, как минимум две части продукта его используют. Если имя файла требует длинного объяснения — вероятно, оно в неправильном месте.

Затем применяйте эти правила в ревью PR. Не превращайте ревью в спор о стиле. Задайте один прямой вопрос: "Будет ли этот файл понятен другому через три месяца?" Если нет — исправьте место до слияния. Это экономит много медленных чисток потом.

Назначьте день очистки до начала большого фич‑сайкла. Один послеобеденный слот часто достаточен, чтобы переименовать папки, переместить очевидные заблудшие файлы и удалить мёртвый код. Делайте это до большого билда: как только фича ложится на запутанное дерево, беспорядок запекается.

Полезная привычка: когда кто‑то не уверен, где лежит файл, пусть спрашивает в PR вместо того, чтобы гадать. Десять минут обсуждения лучше шести месяцев копирования плохого паттерна.

Если приложение уже выглядит спутанным, внешний обзор поможет. Oleg Sotnikov at oleg.is работает как fractional CTO и советник стартапов для растущих команд, и это как раз та проблема, где практическое второе мнение полезно. Иногда решение — не большой рефактор, а ясная карта папок, пара жёстких правил и команда, которая их соблюдает.

Часто задаваемые вопросы

Когда мне нужно переносить код из папки app?

Выносите код из app, когда он будет иметь смысл без привязки к URL. Формы, запросы, валидация и бизнес-правила обычно должны жить в features/..., а в app оставляйте страницы, лэйауты, загрузочные файлы и обработчики маршрутов.

Что должно оставаться в корне проекта?

Держите корень проекта маленьким и однообразным. Большинство команд обходятся app, features, shared, а рядом — файлы конфигурации, скрипты и глобальные типы. Если папка не решает явную задачу на уровне проекта, не ставьте её в корень.

Как понять, что файл принадлежит папке feature?

Спросите, кто владеет правилом или UI. Если это контролируемая одной продуктовой областью логика, держите файл в этой фиче даже если его используют два маршрута. Валидатор биллинга, например, остаётся в billing.

Что считается shared кодом?

Общий код должен работать в нескольких частях продукта и не содержать продуктовых слов. Кнопки, модальные окна, useDebounce или форматтер дат — хорошие кандидаты. BillingPlanCard или useCheckoutFlow — нет.

Нужно ли сохранять топ-уровневые папки components, hooks и utils?

Обычно нет. Верхнеуровневые корзины вроде components, hooks и utils разбрасывают одну фичу по многим местам. Ставьте файлы рядом с фичей, а в shared — только по-настоящему общие вещи.

Где должны жить server actions?

Держите server actions рядом с фичей, которой принадлежит поведение. Если биллинг обновляет план — действие должно лежать в features/billing, а не рядом с app/account/billing/page.tsx. Файлы маршрутов должны связывать страницы, а не хранить бизнес-логику.

Как очистить запутанный репозиторий, не сломав всё?

Начните с одного загруженного кластера маршрутов и перемещайте связанные файлы малыми пачками. После каждого шага поправляйте импорты и запускайте приложение — так вы поймаете ошибки рано. Одна фича за раз даёт маленькие diff'ы и удобные ревью.

Какие признаки того, что структура уходит в сторону?

Смотрите на импорты: длинные относительные пути, расплывчатые папки вроде misc или utils2, и дублированные паттерны — знаки дрейфа. Другой признак — когда коллеги не могут угадать, куда положить новый файл.

Когда переносить код из фичи в shared?

Подождите, пока вторая фича действительно не начнёт использовать код, и убедитесь, что код остаётся общим по смыслу. Если он хранит имена продукта, импорты из фичи или продуктовые правила — пусть он остаётся в фиче. Ранний перенос в shared наполняет его мусором.

Нужен ли полный рефактор, чтобы исправить структуру?

Чаще всего нет. Большинство команд исправляет это спокойной очисткой, короткой картой папок и правилами ревью, которые предотвращают новый мусор. Если команда постоянно спорит или ломает другие части при переносе, полезен быстрый обзор от опытного CTO.