Всем привет! Недавно мы с командой моей студии разработки panfilov.digital запустили новую версию интернет-магазина «Аквилон» (akvilon.kz) – одного из крупных игроков на рынке торговли стройматериалами и товарами для дома в Казахстане.
Пять лет назад мы разработали первую версию магазина на Nuxt 2. За годы поддержки и развития проект превратился в громоздкий монолит, с которым становилось всё сложнее работать. Для бизнеса заказчика это выражалось в конкретных проблемах: сайт стал медленнее открываться под растущей нагрузкой, а внедрение изменений в каталог или логику оформления заказа рисковало превратиться в долгие и дорогостоящие доработки.
С выходом Nuxt 3 перед нами, как и перед всеми, кто поддерживал проекты на второй версии, встала дилемма:
Попытаться «перевезти» проект на новую версию через Nuxt Bridge и постепенную миграцию.
Признать, что код свое отжил, и переписать все с нуля.
Спойлер: мы выбрали рерайт. Попытка миграции, по нашим оценкам, превратила бы наш проект в «зомби» — полуживого монстра из легаси-кода, «мостов» и костылей, поддерживать которого было бы еще дороже.

В этой статье расскажу, почему миграция с Nuxt 2 на 3 для крупных проектов — это иллюзия, как мы выстраивали новую архитектуру на Nuxt 3 и FSD, и почему последующее обновление до Nuxt 4 прошло почти безболезненно.
Почему миграция с Nuxt 2 на 3 была бы «фаталити» для нашего проекта
Когда мы начали изучать вопрос, читая опыт других команд и анализируя свой код, вывод был однозначным: для нашего монолита плавная миграция нереальна. Nuxt 2 → 3 — это не минорное обновление, это мажорный скачок, который ломает всё.
Вот несколько «фаталити», с которыми мы столкнулись бы при попытке сохранить старый код:
Фаталити №1: Composition API vs. Options API
Это не просто новый синтаксис. Это фундаментально иной подход к написанию компонентов.
Вся логика в старом проекте была построена на data, methods, computed и watch из Options API. Nuxt 3 идеологически построен вокруг <script setup> и Composition API. Нам бы всё равно пришлось переписывать практически каждый компонент, чтобы он соответствовал современным стандартам, а не выглядел как "код из 2019-го, запущенный в 2025-м".
Фаталити №2: Стейт-менеджер (Vuex vs. Pinia)
Старый проект использовал Vuex. Новая экосистема Nuxt 3 «заточена» под Pinia. Это не просто замена одного пакета на другой (как axios на ofetch), это полная переписка всей логики управления состоянием и потоков данных.
«Фаталити» №3: Роутинг и получение данных
В Nuxt 2 у нас была сложная, исторически сложившаяся структура в папке pages/ для генерации динамических маршрутов каталога.
? pages
├── ? advice
├── ? brands
├── ? c
│ └── ? _category
│ └── ? _filter.vue
├── ? help
├── ? p
├── ? payment
├── ? personal
├── ? preview
├── ? promo
├── ? _vue
└── ? activate.vue
Старая реализация содержала два типа роутов: /c/elektroinstrumenty/ (категория) и /c/elektroinstrumenty/brands-husqvarna/ (фильтр), и оба они «сидели» на одном динамическом файле _filter.vue.
В новой реализации мы хотели строгого разделения:
_catalog⇒sectionCode(например,/c/elektroinstrumenty/)_filter⇒staticFilter(например,/c/elektroinstrumenty/brands-husqvarna/)
? src
└── ? app
├── ? assets
├── ? composables
├── ? layouts
├── ? middleware
├── ? nuxt-config
└── ? pages
├── ? [staticPage]
├── ? advice
├── ? brands
└── ? c
└── ? [sectionCode]
├── ? [staticFilter].vue
└── ? index.vue
Пытаться натянуть старую логику extendRoutes на новую файловую систему роутинга Nuxt 3 (особенно с их подходом к [...slug].vue) было бы сложнее, чем спроектировать маршрутизацию заново. Это требовало не переноса, а полного переосмысления.
Фаталити №4: Экосистема и пакеты
Команда npm update здесь не спасет. Почти все ключевые UI-библиотеки (например, Swiper Slider) для Nuxt 3 вышли как новые major-версии или были заменены альтернативами. У них кардинально другой API. Нам пришлось бы выкинуть старые реализации слайдеров, модалок, форм валидации и написать их с нуля под новые пакеты.
Строим «правильно» с нуля: Nuxt 3 + FSD + Symfony
Чистый лист дал нам шанс построить архитектуру, которая не развалится через год.
Фундамент: Feature-Sliced Design (FSD)
Мы сразу решили строить проект по методологии FSD. Это позволило разложить сложный e-commerce по понятным «полочкам»: shared, entities, features, widgets, pages.
Это дало нам строгие правила импорта и четкие границы ответственности. Связка с бэкендом на Symfony тоже стала прозрачной: сущность ProductOption на бэке — это entities/product-option на фронте.
Командная работа: Дизайн, Бэк и Фронт в одной упряжке
1. Дизайнер, который смотрит в API
Наш дизайнер Макс перед тем, как рисовать макеты, изучал структуры данных от бэкенда.
Пример: Раньше у нас было два последовательных модальных окна: «Данные покупателя» и «Данные получателя». Изучив API и сценарии, мы объединили их в одно, улучшив UX и упростив логику фронтенда.
2. «Чистый» контракт с бэкендом (Symfony)
Мы договорились о «гигиене» данных. Бэкенд гарантирует предсказуемые типы.
Например, бэк отдает пустую строку "" вместо string | null или data: { a: null } вместо отсутствующего ключа. Это невероятно упростило проверки на фронте — никаких бесконечных if (data && data.a).
Всю сложную бизнес-логику (расчеты цен, скидок, доступных вариантов доставки) мы также жестко зафиксировали на бэкенде. Фронтенд только отображает готовое.
Сила Composition API и паттерн «Фабрика»
Мы выносили всю сложную логику в composables. Самым показательным стал кейс с оформлением заказа, где мы использовали паттерн «Фабрика» для управления тремя типами доставки.
Структура получилась такой:
useOrderBase.ts: содержит общие данные (покупатель, состав корзины).useOrderDelivery.ts: расширяет базу логикой для курьерской доставки.useOrderPickup.ts: расширяет базу логикой для самовывоза.
? composables
├── ? api-data-fetch
├── ? common-catalog
└── ? create-order
└── ? factories
├── ? retail
│ ├── ? base.ts
│ ├── ? delivery.ts
│ └── ? pickup.ts
└── ? wholesale
├── ? base.ts
├── ? delivery.ts
└── ? pickup.ts
Разделяй и властвуй: Auth vs Unauth
Мы следовали принципу «один компонент — одна задача» и избегали «компонентов-богов».
На примере виджета «Сравнение товаров»:
<template>
<AuthCompareWidget v-if="isLoggedIn" />
<UnauthCompareWidget v-else />
</template>
AuthCompareWidget.vue работает с API и базой данных, а UnauthCompareWidget.vue — только с localStorage. Это позволило избежать каши из if-else внутри одного файла.
«Легкий апгрейд»: почему переход с Nuxt 3 на 4 прошел гладко
А теперь — вишенка на торте. Недавно мы обновили наш новый проект с Nuxt 3 до Nuxt 4. И знаете что? Это было несложно.
Контраст с ужасом миграции «2 → 3» был колоссальным. Nuxt 4 стал строже, требуя явного указания ключей в useAsyncData и useFetch, а также предложил новую структуру директории app/.
Почему для нас это прошло гладко?
Мы были готовы. Мы изначально взяли за правило прописывать уникальные
keyдля всехuseAsyncData(чтобы избежать гидратационных ошибок), и это сработало на опережение.FSD-архитектура. Наша структура проекта уже была модульной. Перенос папок под новые стандарты
app/стал простой механической задачейgit mv, а не рефакторингом логики.
Это и есть главная выгода рерайта: правильная архитектура, заложенная на старте, окупается при первом же мажорном обновлении фреймворка.
Что касается метрик производительности сайта (Core Web Vitals): мы еще в процессе их тюнинга. Поскольку приоритетом был полный перезапуск бизнеса с обновленным UX для удержания клиентов, мы не гнались за «зеленой зоной» на старте любой ценой.
Однако главный эффект от перехода на Nuxt 3 мы ощутили сразу — это резкий рост Time-to-Market. Благодаря современной архитектуре новой версии фреймворка (мгновенная сборка на Vite, нативная типизация) мы перестали тратить время на борьбу с инструментами. Разработка стала предсказуемой, доставка новых фич — быстрее, работа сайта — стабильнее.
Итоги: Главные уроки
Рерайт — это не провал, а стратегия. Для легаси-проектов это часто единственное верное решение, которое экономит бюджет в долгосрочной перспективе, отсекая накопленный годами техдолг.
Миграция 2 → 3 — это иллюзия. Для крупного проекта это равносильно рерайту, только гораздо больнее, дороже и с высоким риском создать «зомби-проект» на костылях.
Composition API меняет мышление, а не только синтаксис. Переход сделал разработку более гибкой. Фронтенд фактически сместился в сторону функционального стиля: вместо жесткой структуры Options API мы теперь пишем чистые, переиспользуемые функции (composables).
FSD + Nuxt 3 + TypeScript — это мощная связка для e-commerce. Она дает структуру и масштабируемость, которые не разваливаются при росте команды и функционала.
Культура кода окупается сразу. Наш внутренний стайлгайд, привычка к строгой типизации и явным ключам сделали последующее обновление до Nuxt 4 простой механической задачей, а не очередным авралом.
Надеемся, наш опыт поможет кому-то принять правильное решение: мучиться с миграцией или резать по живому и строить заново.
Готовы ответить на вопросы про FSD и нюансы перезда на Nuxt 3 / 4 в комментариях!
Комментарии (6)

rmrevin
21.11.2025 09:13Статья отличная, но мало технических деталей. С чем конкретно помог фсд? Как разделяете фичи от виджетов? По моему опыту он вносит только сумятицу, т.к. не четко разделяет некоторые концепции. Сущности - это просто модели данных апи? Или там хранится что-то большее. Какие-то альтернативы рассматривались? В общем требуется ещё статья про фсд отдельно)))

DenStrR
21.11.2025 09:13Здравствуйте, я лидировал направление фронтенда при разработке проекта.
С чем конкретно помог FSD?
FSD помог нам победить хаос в структуре проекта.Раньше была директория
components/, и в ней как-то все группировалось по страницам, но в итоге это привело к тому, что код стал иметь сложные перекрестные связи.Если конкретнее: компоненты разных страниц начинали неконтролируемо импортировать друг друга, бизнес-логика перемешивалась с UI-компонентами, и в результате изменение одного, казалось бы, простого компонента могло неожиданно сломать несколько совершенно разных страниц. Мы называли это "эффектом домино" — непредсказуемое распространение изменений по всему приложению.
Теперь у каждого слоя — своя зона ответственности. Это сделало код предсказуемым, а главное — масштабируемым. Когда в команде несколько разработчиков, строгие правила FSD не дают создать «спагетти-код».Как разделяете фичи от виджетов?
· Фича — это полноценный пользовательский сценарий, содержащий бизнес-логику. Например, «Оформление заказа» или «Сравнение товаров». Фича использует сущности и виджеты.
· Виджет — это композиция UI-компонентов для переиспользования в рамках одного проекта. Например, «Блок похожих товаров» или «Хлебные крошки». Он не содержит сложной бизнес-логики.Сущности — это просто модели данных АПИ?
Да,вы угадали. В нашем случае сущности (например, Product, Cart, User) — это в первую очередь типизированные модели данных, которые мы получаем от бэкенда на Symfony. Они определяют «язык» общения между фронтендом и бэкендом.Насчет отдельной статьи по FSD
Спасибо за предложение!Это отличная идея. Уже планируем подготовить более детальный разбор нашей архитектуры с конкретными примерами из проекта «Аквилон». Следите за обновлениями :)

alexss12
21.11.2025 09:13У вас были какие-то проблемы с производительностью после обновления, докручивали что-то в nuxt.config или всё заработало практически из коробки?

DenStrR
21.11.2025 09:13Здравствуйте, я лидировал направление фронтенда при разработке проекта.
Да, некоторые проблемы с производительностью возникли, и в основном они были связаны с нашим первоначальным энтузиазмом по поводу новых возможностей.
Основные моменты, которые пришлось оптимизировать:
Динамические Pinia stores (фабрики) в некоторых сценариях создавали избыточную реактивность
Client-side плагины с watch({ immediate: true }) иногда вызывали каскадные обновления
Местами перегрузили страницы избыточными запросами и вотчерами
Если кратко — мы действительно немного увлеклись новыми возможностями и в некоторых местах "переборщили". В процессе профилирования и тестирования мы выявили эти узкие места и исправили их, оптимизировав количество запросов и реактивных зависимостей.
После этих точечных правок производительность выровнялась до комфортного уровня
danilovmy
Предположу, что перепись произошла у вас безболезненно и потому много сомнительных утверждений и восторгов.
В чем согласен:
Многие удобные и классные библиотеки действительно не работают на nuxt>2 и это единственное что может удерживать долго на 2 версии. И при переезде надо будет переписывать некоторые компоненты фактически с нуля. Если на такой библиотеке построено ядро, то переписывать придется все. И это единственное фаталити, а больше и не нужно, если мы про Mortal Combat.
Быстрый перебилд Vite, конечно, может играть рояль в девелопменте, но для конечного пользователя это незаметно.
В остальном - у вас большая сова на глобусе и просто хотели переписать и переписали:
Options API vs Compositions API.
Вообще то переписывать не надо. options api прекрасно работает в Vue >=2, причем я придерживаюсь мнения, что Options API дает больше чистоты и понимания, чем мешанина всего в setup.
Мнение что только Compositions API позволяет использовать inheritance а Options API нет, говорит о слабом знании Vue. Renderless components и mixIns существовали изначально, просто про них стали говорить чаще с Vue3.
Новая экосистема Nuxt 3 «заточена» под Pinia.
Может наоборот?
Pinia - логичное упрощение Vuex и проще для менее опытных разработчиков. Однако под капотом, старые мутаторы (mutations) спрятаны в сеттеры и только. Причем на сложных мутациях (сложной смене состояний State) - все равно пишем свой код. а Nuxt/Vue вобще все равно, какой state manager вы используете.
Роутинг и получение данных
Говорю же: хотели переписать и переписали.
По статистике, полное переписывание приложения с нуля происходит через 4-6 лет, и вы прекрасно попали в эту статистику. (Forrester research, full refactoring for business apps after 4–7 years).
Вы решили свои бизнес задачи, молодцы!
Но вот с переездом Nuxt2-Nuxt3-Nuxt4 это связано очень опосредованно. Если бы у вас был талантливый программист на react, angular, svelte или, прости господи, $mal, который успешно помог бы переехать - была бы очень похожая статья просто с другими названиями.
В любом случае @mpanfilov - спасибо за описание положительного опыта переезда. Можете рассказать,, почему остановились на использовании Swiper Slider и пробовали ли Splider?
DenStrR
Здравствуйте, я лидировал направление фронтенда при разработке проекта.
Благодарим за глубокий анализ и ваш взгляд!
Вы правы в том, что технически Options API продолжает работать в Vue 3, а Pinia не является строго обязательной. Однако для нашего конкретного случая — крупного e-commerce монолита со сложной бизнес-логикой — именно Composition API показал себя более эффективным для выделения переиспользуемой логики в composables, что критично для поддержки и масштабирования.
Что касается роутинга — вы точно подметили, что мы действительно хотели переосмыслить архитектуру, а не просто перенести старые решения. Рерайт дал нам эту возможность без ограничений legacy-кода.
По слайдеру: на момент выбора мы не знали о Splider, поэтому продолжили использовать Swiper, теперь есть над чем задуматься:)
Еще раз спасибо за ценные замечания!