What actually changes: build pipeline (Nitro), file-based routing conventions, async data APIs, plugin signatures, and often hosting ( SSR and hydration). Treat each as a workstream with owners—otherwise the team debates “one big PR” for six months.
1. Module compatibility matrix
Inventory @nuxtjs/* and community modules. Many have Nuxt 3–native successors; some require replacement (auth, PWA, i18n in particular). A greenfield roadmap session should list these before line one of the code port.
2. Data fetching and stores
Map Nuxt 2 asyncData/fetch to Nuxt 3 patterns (useAsyncData / useFetch). Pair with Pinia for client state. This layer is where subtle SSR bugs (double fetch, state leakage) appear.
3. When Bridge helps—and when it delays
Bridge can unblock incremental movement, but it is a phase with its own tax. Budget time or you trade a “Bridge forever” org chart problem for the EOL date.
4. Routing and layouts: convention deltas
File-based routing looks similar on the surface, but the conventions changed in ways that bite during the port:
| Concern | Nuxt 2 | Nuxt 3 |
|---|---|---|
| Dynamic route | _id.vue | [id].vue |
| Catch-all | _.vue | [...slug].vue |
| Layouts | layout: 'admin' in component options | definePageMeta({ layout: 'admin' }) |
| Middleware | Global or per-page string | defineNuxtRouteMiddleware + definePageMeta |
| Error page | layouts/error.vue | error.vue at app root |
Vue Router itself jumps from 3 to 4 underneath, with its own breaking changes—see router 3 to 4. Layouts that overrode parent slots in v2 typically need a small refactor on Nuxt 3.
5. Plugins and module hooks
Nuxt 2 plugin signatures (({ app, store }, inject) => { ... }) move to defineNuxtPlugin:
// plugins/api.ts (Nuxt 3)
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig()
const api = $fetch.create({ baseURL: config.public.apiBase })
return {
provide: { api },
}
})
// usage:
// const { $api } = useNuxtApp()
Two patterns deserve special attention. First, anything that injected into context.app (like custom error handlers) usually moves to a plugin or a app.config.ts entry. Second, server-only logic that ran in Nuxt 2 plugins must move to a Nitro server route or a server/ handler—Nuxt 3 keeps server and client plugins separated more strictly.
6. Hosting and deployment changes
Nitro decouples Nuxt from a specific Node server runtime. The same codebase can target Node, Vercel, Netlify, Cloudflare Workers, AWS Lambda, or static export. That flexibility is great—and a project risk if your ops team is not in the loop. Decide the deployment target before the cutover, not after.
Concrete migration questions to answer:
- Are we keeping a long-running Node server, moving to serverless functions, or going edge?
- What is the cold-start tolerance for our slowest route? (Edge runtimes have surprisingly different rules from Lambda.)
- How do environment secrets flow into
runtimeConfig? The Nuxt 2 pattern of readingprocess.envdirectly in components stops working at build time. - Do we still need a custom
server/index.js, or can Nitro handlers replace it entirely?
If hosting answers are unclear, consider Nuxt Bridge as a phase that decouples the Vue 3 work from the runtime change.
7. Testing strategy during the port
Nuxt apps fail interestingly because so much can go subtly wrong on the SSR/hydration boundary. The tests that catch real regressions:
- Hydration smoke test on the top 10 routes. Render server-side, then mount in a real browser and assert the DOM equals the SSR output. Mismatches usually reveal misuses of
useStatevs. component-local refs. - Data fetching contract tests. Confirm
useAsyncDatakeys are stable across renders—forgetting the key argument causes double-fetching that only shows up under load. - Component tests for migrated UIs. Run them with Vue Test Utils and a couple of Cypress component sweeps for the trickiest forms.
- Performance budget on first byte and LCP. Compare to your Nuxt 2 baseline. Don't trust dev-mode numbers—use field measurements.
8. Migration checklist for Nuxt teams
- Inventory every
@nuxtjs/*and community module; map to Nuxt 3 successor or replacement. - List all
asyncData/fetchhooks; planuseAsyncData/useFetchequivalents with explicit keys. - Document every plugin's purpose; flag the ones that touch
context.appor inject globals. - Pick a deployment target with the ops team in the room.
- Decide on Pinia store layout before the first store is ported.
- Lock in TypeScript strictness expectations early; Nuxt 3 is much more TS-native than Nuxt 2.
- Write a short Bridge-or-not memo with named trade-offs for leadership.
- Schedule a scope freeze window aligned with the deepest cutover work.
9. FAQ
Should we go Bridge or skip straight to Nuxt 3?
If your modules and hosting are ready, skip Bridge. If not, Bridge is a useful phase—but only when the Nuxt 3 cutover is funded too. The detailed decision criteria live in Nuxt Bridge: stepping stone or second migration.
Is Nuxt 3 always faster than Nuxt 2?
In dev (Vite), yes—dramatically. In production, Nitro is usually faster, especially on edge runtimes; but a misconfigured deployment can be slower than the Node app it replaced. Measure on real traffic.
Can we run Nuxt 2 and Nuxt 3 side by side?
Yes, behind a reverse proxy. The strangler fig pattern works for Nuxt as well as plain Vue, especially when one team is on marketing pages and another on the app shell.
How long does it really take?
For a medium SaaS with ~80 routes and a few custom modules, 12–20 focused engineering weeks is realistic. Smaller marketing sites can ship in 4–6 weeks; large platforms with custom Nuxt 2 modules take a quarter or more. See timeline framing.
Nuxt in production? We can scope it.
Platform migrations with honest timelines and module-by-module plans.
Book a Nuxt callConclusion
Nuxt 2 to 3 is an architecture project: server, build, and app layers move together. Name the workstreams, validate modules, and do not call it a “Vue 3 re-skin” in your steering deck—it is not one.
