Nuxt 2 to Nuxt 3 Migration: A Practical Guide with Nuxt Bridge
AI generated
<v/>
{ }
Nuxt · Vue 3 · Migration · Nuxt Bridge
Nuxt 2 to Nuxt 3 Migration
a practical guide without a big-bang rewrite

Rewriting a large project from scratch for a Nuxt 2 to Nuxt 3 migration is rarely realistic. With Nuxt Bridge as an intermediate step and a clear order for Vuex, asyncData and module replacement, the migration can happen in manageable, testable stages instead of stalling the project for weeks.

22 min read Nuxt Bridge · Pinia · useAsyncData · modules Nuxt 2.17 · Nuxt 3.x · Vue 3

1. Why a Nuxt 2 to Nuxt 3 migration is not a trivial upgrade

A Nuxt 2 to Nuxt 3 migration is fundamentally different from a routine minor update. Nuxt 3 is built on Vue 3, uses an entirely new server engine called Nitro, and replaces many of the concepts a Nuxt 2 project is built on: Vuex gives way to Pinia, the Options-API-heavy structure gives way to the Composition API, and most of the module ecosystem had to be rewritten. Anyone who underestimates the migration and simply bumps the package version runs into a cascade of build errors that barely hint at the real cause.

The critical mistake many teams make: treating a Nuxt 2 to Nuxt 3 migration as a pure technical update rather than a project with its own risk profile. Larger Nuxt 2 applications accumulate modules over years, and some of them never got a Nuxt 3 equivalent at all. Those exact dependencies end up deciding whether the migration takes weeks or months. The sections below walk through an approach built on Nuxt Bridge as an intermediate step, rather than rewriting the whole project in one go.

2. Preparation: taking stock before you start

Before touching a single line of code, a successful Nuxt 2 to Nuxt 3 migration needs an honest inventory. Which Nuxt modules are in use, and does each one have a maintained Nuxt 3 equivalent? How deeply is Vuex embedded in the codebase, and how many components still use the Options API with this.$store? How many pages rely on asyncData or the old fetch hook variant from Nuxt 2? Without this list, the effort of the migration cannot be estimated realistically, and every time estimate stays pure speculation.

A practical approach: a dependency audit with npm ls combined with a grep search for this.$store, asyncData( and Vue.extend quickly gives a quantitative view of the migration scope. Projects with more than fifty components still relying on the Options API with global store access benefit greatly from splitting the migration into several phases rather than tackling everything at once. In practice, the Nuxt 2 to Nuxt 3 migration almost always goes better when Bridge, store and data fetching are handled as separate tracks.


# Audit: which patterns still exist in the Nuxt 2 codebase
grep -rl "this.\$store" src/ | wc -l
grep -rl "asyncData(" pages/ | wc -l
grep -rl "Vue.extend" src/ components/ | wc -l

# List installed Nuxt modules and cross-check for Nuxt 3 equivalents
npm ls --depth=0 | grep "nuxt-"

# Check current Nuxt and Vue versions before starting
npx nuxt --version
npm ls vue

3. Using Nuxt Bridge as a safety net

Nuxt Bridge is the central tool for a low-risk Nuxt 2 to Nuxt 3 migration. It brings many Nuxt 3 APIs, including the Composition API, the Nitro server engine and Vite support, back into an existing Nuxt 2 project without requiring an immediate switch to Nuxt 3 itself. The key benefit: the project stays runnable and deployable throughout the entire Bridge phase. Teams can move component by component onto new patterns while the rest of the application keeps running on Nuxt 2 with Bridge enabled.

Installation happens through the @nuxt/bridge package, which replaces the existing nuxt dependency in package.json. After installation, the build process immediately shows which modules are incompatible, since Bridge internally already uses Nitro instead of the old Nuxt 2 server middleware. This early feedback is a major advantage over jumping straight to Nuxt 3: incompatibilities become visible while the application is still running in production, instead of only after the full switch.


# Install Nuxt Bridge instead of jumping straight to Nuxt 3
npm install @nuxt/bridge@npm:@nuxt/bridge-edge -D
npm uninstall nuxt

# nuxt.config.js - enable bridge features incrementally
export default {
  bridge: {
    vite: false,        // enable once build issues are resolved
    nitro: true,        // new server engine, replaces serverMiddleware
    composition: true,  // Composition API available in Options API components
  },
}

# Run dev server with bridge active and watch for compatibility warnings
npm run dev

4. From nuxt.config.js to nuxt.config.ts

A visible but technically manageable part of the Nuxt 2 to Nuxt 3 migration is updating the configuration file. Nuxt 3 expects defineNuxtConfig() instead of a raw object export, and many top-level options from Nuxt 2 have been renamed or moved into subgroups. modules is still there, but buildModules from Nuxt 2 no longer exists as a separate concept, all build-time modules now live together under modules. env is replaced by runtimeConfig, which cleanly separates server-side and client-side environment variables.

For projects with a large configuration, switching the config file itself to TypeScript is worthwhile even if the rest of the code stays JavaScript. defineNuxtConfig() provides full type information for every option, so typos in configuration keys that Nuxt 2 silently ignored now surface at compile time. That is especially valuable during a migration when many configuration values are being rewritten at once.


// nuxt.config.ts - Nuxt 3 configuration replacing nuxt.config.js
export default defineNuxtConfig({
  modules: [
    '@pinia/nuxt',       // replaces the old Vuex store setup
    '@nuxtjs/tailwindcss',
  ],

  // env (Nuxt 2) is replaced by runtimeConfig with public/private split
  runtimeConfig: {
    apiSecret: process.env.API_SECRET,     // server-side only
    public: {
      apiBase: process.env.API_BASE_URL,   // exposed to the client
    },
  },

  // buildModules from Nuxt 2 merges into modules in Nuxt 3
  nitro: {
    preset: 'node-server',
  },
})

5. Replacing Vuex with Pinia

The store switch is, content-wise, the biggest single step of any Nuxt 2 to Nuxt 3 migration. Pinia is the official successor to Vuex in the Nuxt 3 ecosystem and brings noticeably less boilerplate: no more mutations, direct state changes inside actions, and full TypeScript inference without extra type definitions. Migrating a Vuex module to a Pinia store follows a clear pattern: state becomes a function returning the initial state, getters stay structurally almost identical, and mutations plus actions merge into plain actions methods with direct this.property = value access.

In practice, Vuex modules are migrated one at a time, not the entire store structure in one go. Since Pinia can be installed alongside Vuex, new features can be written directly in Pinia while existing Vuex modules are transferred incrementally. This incremental strategy prevents the Nuxt 2 to Nuxt 3 migration from getting stuck on one giant store refactor that only becomes testable again after weeks.


// stores/cart.js - Pinia store replacing the old Vuex cart module
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [],
    isLoading: false,
  }),

  getters: {
    // getters stay structurally close to Vuex getters
    itemCount: (state) => state.items.length,
    total: (state) => state.items.reduce((sum, i) => sum + i.price * i.qty, 0),
  },

  actions: {
    // mutations and actions merge into plain methods with direct state writes
    async addItem(product) {
      this.isLoading = true
      try {
        const existing = this.items.find((i) => i.id === product.id)
        if (existing) {
          existing.qty += 1
        } else {
          this.items.push({ ...product, qty: 1 })
        }
      } finally {
        this.isLoading = false
      }
    },
  },
})

6. Replacing asyncData and fetch with useAsyncData and useFetch

Data fetching is the second major building block of any Nuxt 2 to Nuxt 3 migration. The Options API hooks asyncData() and fetch() from Nuxt 2 no longer exist in their old form in Nuxt 3, but are replaced by the composables useAsyncData() and useFetch(), called inside setup() or script setup. The key difference: the new composables return reactive refs, not properties automatically merged onto the component. That forces a more explicit handling of loading states, errors and data, but in exchange delivers better type inference and simpler testing.

A common migration mistake: developers call useFetch() inside an onMounted() hook because that resembles the old fetch() behavior. That works, but loses the decisive advantage, namely server-side rendering of the fetched data. useFetch() must sit at the top level of setup() so that Nuxt can resolve the call correctly during the SSR pass and serialize the state for hydration.


// Nuxt 2: Options API with asyncData
export default {
  async asyncData({ $axios, params }) {
    const product = await $axios.$get(`/api/products/${params.id}`)
    return { product }
  },
}

// Nuxt 3: useAsyncData inside script setup - must stay top-level for SSR
const route = useRoute()
const { data: product, pending, error, refresh } = await useAsyncData(
  `product-${route.params.id}`,
  () => $fetch(`/api/products/${route.params.id}`)
)

// useFetch is a thin wrapper for the common REST case
const { data: reviews } = await useFetch(`/api/products/${route.params.id}/reviews`, {
  key: `reviews-${route.params.id}`,
})

7. Auto-imports and the changed directory structure

Nuxt 3 introduces auto-imports for composables, components and core Vue functions like ref and computed, which drastically reduces import boilerplate but also requires some adjustment during the Nuxt 2 to Nuxt 3 migration. Files in the composables/ directory are automatically available in every component without an explicit import. For migration projects that means existing utility functions from Nuxt 2, previously imported manually, become available everywhere in the project without an import line once moved into composables/.

The directory structure itself has also changed. store/ disappears in favor of Pinia stores, usually placed under stores/. The plugins/ directory stays, but expects a different export format using defineNuxtPlugin(). Middleware moves from middleware/ with an Options API signature to functions using defineNuxtRouteMiddleware(). These structural changes touch almost every file in the project, which is why they are well suited to automated codemods rather than editing each file by hand.

8. Module ecosystem: retiring or replacing Nuxt 2 modules

The biggest uncertainty in any Nuxt 2 to Nuxt 3 migration rarely lies in your own code, but in third-party modules. Popular Nuxt 2 modules like @nuxtjs/auth were either rewritten from scratch, as with @sidebase/nuxt-auth, or quietly abandoned without an official Nuxt 3 equivalent. These exact cases end up deciding the duration of the migration, because missing modules must either be rebuilt in-house or replaced through fundamentally different architecture decisions.

A pragmatic approach: for every module in the inventory, check whether an actively maintained Nuxt 3 module exists, whether the functionality has since become part of Nuxt 3 core, or whether a small, custom composable can fully replace the dependency. For simple modules that only wrapped a few lines of logic, replacing them with a hand-written composable is often worthwhile rather than relying on a possibly immature third-party community module.

9. Nuxt 2 and Nuxt 3 side by side

The table below summarizes the key concept changes that appear in practically every Nuxt 2 to Nuxt 3 migration. Use it as a checklist so none of the central changes gets overlooked in your migration plan.

Area Nuxt 2 Nuxt 3 Migration effort
State management Vuex with mutations Pinia with direct actions Medium to high
Data fetching asyncData / fetch hook useAsyncData / useFetch Medium
Configuration nuxt.config.js object defineNuxtConfig() with types Low
Server engine connect-based middleware Nitro (server/api/) High
Imports Manual imports everywhere Auto-imports for composables Low

The table makes clear that the effort is not evenly distributed. The server-side engine and the store switch are the most demanding parts of any Nuxt 2 to Nuxt 3 migration, while configuration and imports can usually be automated with manageable effort. Anyone planning the migration should factor this ordering into the time estimate rather than weighting all areas equally.

Mironsoft

Vue and Nuxt migrations without big-bang risk

Nuxt 2 to Nuxt 3 migration without production downtime?

We plan and support your Nuxt 2 to Nuxt 3 migration with Nuxt Bridge as an intermediate step, migrate Vuex to Pinia incrementally, and check your module ecosystem for Nuxt 3 compatibility.

Migration audit

Systematically capture module compatibility, store scope and data-fetching patterns

Incremental migration

Nuxt Bridge, the Pinia switch and useAsyncData migration step by step

Module replacement

Replace missing Nuxt 3 modules with lean composables

10. Summary

A Nuxt 2 to Nuxt 3 migration succeeds most reliably when it is not treated as a one-time rewrite but as a sequence of clearly separated phases. Nuxt Bridge lowers the risk because the project stays runnable throughout the entire transition and incompatibilities surface early. Vuex modules move to Pinia one at a time, asyncData and fetch calls get replaced page by page with useAsyncData and useFetch, and the module audit at the start prevents nasty surprises in the middle of the migration.

The biggest lever is ordering: configuration and auto-imports first, because they are low-risk and quick to finish, then store migration and data fetching as larger blocks, and module replacement in parallel once it is clear which dependencies are actually affected. Following this order significantly reduces the risk of a Nuxt 2 to Nuxt 3 migration without making the project unproductive for weeks.

Nuxt 2 to Nuxt 3 Migration — The Essentials at a Glance

Nuxt Bridge first

Bridge brings Nuxt 3 APIs back into a running Nuxt 2 project without an immediate full switch. Significantly reduces migration risk.

Vuex to Pinia incrementally

Migrate modules one at a time, not the whole store structure at once. Pinia can be installed alongside Vuex.

Switch data fetching

asyncData and fetch are replaced by useAsyncData and useFetch. Call must stay top-level in setup(), otherwise no SSR.

Module audit upfront

Check every Nuxt 2 module for a Nuxt 3 equivalent before the migration starts. Prevents surprises mid-project.

11. FAQ: Nuxt 2 to Nuxt 3 Migration

1How long does a Nuxt 2 to Nuxt 3 migration take?
Between a few days and several months, depending on project size and module dependencies. Nuxt Bridge lowers risk but does not directly shorten duration.
2Do I have to use Nuxt Bridge?
No, but for medium to large projects it is significantly less risky than jumping directly. Incompatibilities surface early while the application runs in production.
3Use Vuex and Pinia in parallel?
Yes, both can be installed together. New features in Pinia, existing Vuex modules migrated incrementally, Vuex fully removed at the end.
4What happens to asyncData and fetch?
Replaced by useAsyncData and useFetch, called in setup() or script setup. Return reactive refs instead of automatically merged properties.
5Why no SSR data from useFetch in onMounted?
useFetch must sit top-level in setup() for Nuxt to resolve it during SSR. Inside onMounted it only runs client-side.
6Nuxt 2 modules with no Nuxt 3 equivalent?
Check first if the functionality is now part of Nuxt 3 core. Otherwise look for an alternative module or write a custom composable for manageable logic.
7Does the directory structure change?
store/ becomes stores/ with Pinia, plugins/ stays with defineNuxtPlugin(), middleware/ uses defineNuxtRouteMiddleware() instead of Options API signatures.
8What do auto-imports give me?
Composables and core Vue functions are automatically available everywhere. Utility functions from Nuxt 2 benefit right after moving to composables/.
9Switch nuxt.config.js to TypeScript?
Recommended, even with otherwise plain JavaScript code. defineNuxtConfig() with TypeScript surfaces typos in configuration keys immediately.
10Biggest time sink in the migration?
Usually third-party modules without a maintained Nuxt 3 equivalent. A thorough module audit at the start prevents them from blocking the process midway.