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.
Table of Contents
- 1. Why a Nuxt 2 to Nuxt 3 migration is not a trivial upgrade
- 2. Preparation: taking stock before you start
- 3. Using Nuxt Bridge as a safety net
- 4. From nuxt.config.js to nuxt.config.ts
- 5. Replacing Vuex with Pinia
- 6. Replacing asyncData and fetch with useAsyncData and useFetch
- 7. Auto-imports and the changed directory structure
- 8. Module ecosystem: retiring or replacing Nuxt 2 modules
- 9. Nuxt 2 and Nuxt 3 side by side
- 10. Summary
- 11. FAQ
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.