Всем привет! Недавно мы с командой моей студии разработки panfilov.digital запустили новую версию интернет-магазина «Аквилон» (akvilon.kz) – одного из крупных игроков на рынке торговли стройматериалами и товарами для дома в Казахстане.

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

С выходом Nuxt 3 перед нами, как и перед всеми, кто поддерживал проекты на второй версии, встала дилемма:

  1. Попытаться «перевезти» проект на новую версию через Nuxt Bridge и постепенную миграцию.

  2. Признать, что код свое отжил, и переписать все с нуля.

Спойлер: мы выбрали рерайт. Попытка миграции, по нашим оценкам, превратила бы наш проект в «зомби» — полуживого монстра из легаси-кода, «мостов» и костылей, поддерживать которого было бы еще дороже.

Главная страница новой версии интернет-магазина «Аквилон» на Nuxt
Главная страница новой версии интернет-магазина «Аквилон» на Nuxt

В этой статье расскажу, почему миграция с 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.

В новой реализации мы хотели строгого разделения:

  • _catalogsectionCode (например, /c/elektroinstrumenty/)

  • _filterstaticFilter (например, /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/.

Почему для нас это прошло гладко?

  1. Мы были готовы. Мы изначально взяли за правило прописывать уникальные key для всех useAsyncData (чтобы избежать гидратационных ошибок), и это сработало на опережение.

  2. FSD-архитектура. Наша структура проекта уже была модульной. Перенос папок под новые стандарты app/ стал простой механической задачей git mv, а не рефакторингом логики.

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

Что касается метрик производительности сайта (Core Web Vitals): мы еще в процессе их тюнинга. Поскольку приоритетом был полный перезапуск бизнеса с обновленным UX для удержания клиентов, мы не гнались за «зеленой зоной» на старте любой ценой.

Однако главный эффект от перехода на Nuxt 3 мы ощутили сразу — это резкий рост Time-to-Market. Благодаря современной архитектуре новой версии фреймворка (мгновенная сборка на Vite, нативная типизация) мы перестали тратить время на борьбу с инструментами. Разработка стала предсказуемой, доставка новых фич — быстрее, работа сайта — стабильнее.


Итоги: Главные уроки

  1. Рерайт — это не провал, а стратегия. Для легаси-проектов это часто единственное верное решение, которое экономит бюджет в долгосрочной перспективе, отсекая накопленный годами техдолг.

  2. Миграция 2 → 3 — это иллюзия. Для крупного проекта это равносильно рерайту, только гораздо больнее, дороже и с высоким риском создать «зомби-проект» на костылях.

  3. Composition API меняет мышление, а не только синтаксис. Переход сделал разработку более гибкой. Фронтенд фактически сместился в сторону функционального стиля: вместо жесткой структуры Options API мы теперь пишем чистые, переиспользуемые функции (composables).

  4. FSD + Nuxt 3 + TypeScript — это мощная связка для e-commerce. Она дает структуру и масштабируемость, которые не разваливаются при росте команды и функционала.

  5. Культура кода окупается сразу. Наш внутренний стайлгайд, привычка к строгой типизации и явным ключам сделали последующее обновление до Nuxt 4 простой механической задачей, а не очередным авралом.

Надеемся, наш опыт поможет кому-то принять правильное решение: мучиться с миграцией или резать по живому и строить заново.

Готовы ответить на вопросы про FSD и нюансы перезда на Nuxt 3 / 4 в комментариях!

Комментарии (6)


  1. danilovmy
    21.11.2025 09:13

    Предположу, что перепись произошла у вас безболезненно и потому много сомнительных утверждений и восторгов.

    В чем согласен:

    Многие удобные и классные библиотеки действительно не работают на 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?


    1. DenStrR
      21.11.2025 09:13

      Здравствуйте, я лидировал направление фронтенда при разработке проекта.

      Благодарим за глубокий анализ и ваш взгляд!

      Вы правы в том, что технически Options API продолжает работать в Vue 3, а Pinia не является строго обязательной. Однако для нашего конкретного случая — крупного e-commerce монолита со сложной бизнес-логикой — именно Composition API показал себя более эффективным для выделения переиспользуемой логики в composables, что критично для поддержки и масштабирования.

      Что касается роутинга — вы точно подметили, что мы действительно хотели переосмыслить архитектуру, а не просто перенести старые решения. Рерайт дал нам эту возможность без ограничений legacy-кода.

      По слайдеру: на момент выбора мы не знали о Splider, поэтому продолжили использовать Swiper, теперь есть над чем задуматься:)

      Еще раз спасибо за ценные замечания!


  1. rmrevin
    21.11.2025 09:13

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


    1. DenStrR
      21.11.2025 09:13

      Здравствуйте, я лидировал направление фронтенда при разработке проекта.

      С чем конкретно помог FSD?
      FSD помог нам победить хаос в структуре проекта.

      Раньше была директория components/, и в ней как-то все группировалось по страницам, но в итоге это привело к тому, что код стал иметь сложные перекрестные связи.

      Если конкретнее: компоненты разных страниц начинали неконтролируемо импортировать друг друга, бизнес-логика перемешивалась с UI-компонентами, и в результате изменение одного, казалось бы, простого компонента могло неожиданно сломать несколько совершенно разных страниц. Мы называли это "эффектом домино" — непредсказуемое распространение изменений по всему приложению.


      Теперь у каждого слоя — своя зона ответственности. Это сделало код предсказуемым, а главное — масштабируемым. Когда в команде несколько разработчиков, строгие правила FSD не дают создать «спагетти-код».

      Как разделяете фичи от виджетов?

      · Фича — это полноценный пользовательский сценарий, содержащий бизнес-логику. Например, «Оформление заказа» или «Сравнение товаров». Фича использует сущности и виджеты.
      · Виджет — это композиция UI-компонентов для переиспользования в рамках одного проекта. Например, «Блок похожих товаров» или «Хлебные крошки». Он не содержит сложной бизнес-логики.

      Сущности — это просто модели данных АПИ?
      Да,вы угадали. В нашем случае сущности (например, Product, Cart, User) — это в первую очередь типизированные модели данных, которые мы получаем от бэкенда на Symfony. Они определяют «язык» общения между фронтендом и бэкендом.

      Насчет отдельной статьи по FSD
      Спасибо за предложение!Это отличная идея. Уже планируем подготовить более детальный разбор нашей архитектуры с конкретными примерами из проекта «Аквилон». Следите за обновлениями :)


  1. alexss12
    21.11.2025 09:13

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


    1. DenStrR
      21.11.2025 09:13

      Здравствуйте, я лидировал направление фронтенда при разработке проекта.

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

      Основные моменты, которые пришлось оптимизировать:

      Динамические Pinia stores (фабрики) в некоторых сценариях создавали избыточную реактивность

      Client-side плагины с watch({ immediate: true }) иногда вызывали каскадные обновления

      Местами перегрузили страницы избыточными запросами и вотчерами

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

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