Всем привет! Недавно мы с командой моей студии разработки 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 в комментариях!