Nuxt Upgrade Guide

Compare Nuxt versions and see exactly what changes, what requires action, and what you can start using.

→
12Changes
10Breaking
12Actions
0Features
2Migration tools
Analyze your project

Check your package.json

UpgradePath detects relevant tools and selects them automatically.

Local only
Analyzed locally in your browser. Nothing is uploaded.
Project setup

What does your project use?

Select the tools in your project to include relevant dependency updates in this guide.

Breaking Changes

Configuration uses defineNuxtConfig and ESM

Action required
breakinghigh impact

Nuxt 3 rewrites the configuration and build system. CommonJS configuration and legacy buildModules need migration.

Beforetypescript
module.exports = {
  buildModules: ['@nuxtjs/tailwindcss']
}
Aftertypescript
export default defineNuxtConfig({
  modules: ['@nuxtjs/tailwindcss']
})
Migration

Convert the config to ESM. Move compatible modules to modules and verify each module supports Nuxt 3.

Vue 3 replaces Vue 2

Action required
breakinghigh impact

Nuxt 3 uses Vue 3 and Vue Router 4. Vue 2 plugins, filters and removed APIs need review; the Options API remains supported.

Beforevue
<template>{{ price | currency }}</template>
<script>
export default { filters: { currency: v => '$' + v } }
</script>
Aftervue
<script setup>
const props = defineProps({ price: Number })
const formatted = computed(() => '$' + props.price)
</script>
<template>{{ formatted }}</template>
Migration

Update Vue dependencies together and replace filters with methods or computed values. Check third-party Vue compatibility.

asyncData and fetch migrate to SSR composables

Action required
breakinghigh impact

useAsyncData and useFetch transfer server-fetched data through the Nuxt payload. A direct $fetch in component setup can fetch again during hydration.

Beforevue
<script>
export default {
  async asyncData({ $axios }) {
    return { posts: await $axios.$get('/api/posts') }
  }
}
</script>
Aftervue
<script setup>
const { data: posts, error, status } = await useFetch('/api/posts')
</script>
Migration

Use useFetch for URLs or useAsyncData for custom handlers. Return serializable data and handle pending/error states.

Plugins use defineNuxtPlugin and provide

Action required
breakinghigh impact

The context/inject plugin signature is replaced by a Nuxt app plugin. Plugins in plugins/ are registered automatically.

Beforetypescript
export default (context, inject) => {
  inject('hello', name => 'Hello ' + name)
}
Aftertypescript
// plugins/hello.ts
export default defineNuxtPlugin(() => ({
  provide: { hello: (name: string) => 'Hello ' + name }
}))
// In component setup:
const { $hello } = useNuxtApp()
Migration

Return helpers through provide and use useNuxtApp to consume them. Name browser-only plugins with .client.ts.

Route middleware uses navigation return values

Action required
breakinghigh impact

defineNuxtRouteMiddleware receives to/from rather than Nuxt 2 context. Redirects and aborts must be returned.

Beforetypescript
export default function ({ store, redirect }) {
  if (!store.state.user) return redirect('/login')
}
Aftertypescript
export default defineNuxtRouteMiddleware(() => {
  const user = useState('user', () => null)
  if (!user.value) return navigateTo('/login')
})
Migration

Move middleware to the new signature, return navigateTo/abortNavigation and use .global.ts for global route middleware.

Dynamic route filenames use brackets

Action required
breakinghigh impact

Nuxt 2 underscore route conventions change to Vue Router 4-style bracket filenames.

Beforetext
pages/users/_id.vue
pages/_.vue
Aftertext
pages/users/[id].vue
pages/[...slug].vue
Migration

Rename dynamic/catch-all files and verify optional parameters, generated routes and direct SSR requests.

Layouts use slots and NuxtLayout/NuxtPage

Action required
breakinghigh impact

The Nuxt 2 Nuxt and NuxtChild components are replaced. Layout and page settings move to definePageMeta.

Beforevue
<!-- layouts/default.vue -->
<template><main><Nuxt /></main></template>
Aftervue
<!-- layouts/default.vue -->
<template><main><slot /></main></template>
<!-- app.vue -->
<template><NuxtLayout><NuxtPage /></NuxtLayout></template>
Migration

Use slot in layouts and NuxtPage for pages/nested pages. Wrap the root page in NuxtLayout when using layouts.

Runtime config separates server secrets from public values

Action required
breakinghigh impact

publicRuntimeConfig/privateRuntimeConfig become runtimeConfig with a public section. Public values are exposed to the browser.

Beforetypescript
export default {
  privateRuntimeConfig: { apiSecret: '' },
  publicRuntimeConfig: { apiBase: '/api' }
}
Aftertypescript
export default defineNuxtConfig({
  runtimeConfig: {
    apiSecret: '', // NUXT_API_SECRET; server only
    public: { apiBase: '/api' } // NUXT_PUBLIC_API_BASE
  }
})
Migration

Keep secrets at the root and read configuration using useRuntimeConfig. Use matching NUXT_ environment variables at runtime.

Nitro replaces legacy serverMiddleware

Action required
breakinghigh impact

Nuxt 3 uses the standalone Nitro server with file-based API routes and H3 handlers.

Beforetypescript
// nuxt.config.js
export default { serverMiddleware: [
  { path: '/api/health', handler: '~/api/health.js' }
] }
// api/health.js
export default (req, res) => res.end('ok')
Aftertypescript
// server/api/health.get.ts
export default defineEventHandler(() => ({ status: 'ok' }))
Migration

Move Express/connect middleware to compatible Nitro handlers. Preserve authentication, status codes and response semantics.

Deploy the Nitro output rather than a Nuxt 2 server

Action required
breakinghigh impact

Node deployments run .output/server/index.mjs. Nuxt 2 nuxt start and .nuxt directory deployment instructions do not match Nitro.

Beforebash
npm run build
npx nuxt start
Afterbash
npm run build
node .output/server/index.mjs
Migration

Build for the intended Nitro preset and ship the complete .output. Check SSR pages, environment values, static assets and health endpoints.

Migration Tools

Head metadata moves from vue-meta to useHead

Action required
migrationmedium impact

Nuxt 3 uses Unhead. useHead/useSeoMeta support reactive SSR metadata; duplicate-tag identifiers such as hid are unnecessary.

Beforevue
<script>
export default {
  head: () => ({ meta: [{ hid: 'description', name: 'description', content: 'Upgrade guide' }] })
}
</script>
Aftervue
<script setup>
useSeoMeta({ title: 'Upgrade guide', description: 'Upgrade guide' })
useHead({ link: [{ rel: 'canonical', href: 'https://example.com/guide' }] })
</script>
Migration

Migrate page titles, descriptions and canonical links, then inspect server HTML. Do not expose server-only values in metadata.

SSR state must be request-safe

Action required
migrationmedium impact

Nuxt 3 has no built-in Vuex integration. useState creates shared SSR-safe state; module-scope refs can leak state between server requests.

Beforetypescript
// Shared across requests when imported by server code:
export const user = ref(null)
Aftertypescript
// Call from component setup or a Nuxt composable:
export const useUser = () => useState('user', () => null)
Migration

Migrate small state to useState or install compatible Pinia integration. Keep user-specific state inside Nuxt request context.