Nuxt Upgrade Guide

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

→
16Changes
6Breaking
15Actions
0Features
1Migration 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

Node.js minimum runtime increases

Action required
breakinghigh impact

Nuxt 4.0.0 requires Node.js ^20.19.0 or >=22.12.0. Historical Nuxt 3 runtime support does not carry over.

Beforejson
{ "engines": { "node": ">=18" } }
Afterjson
{ "engines": { "node": "^20.19.0 || >=22.12.0" } }
Migration

Update development, CI and production runtimes. Use a currently supported Node.js release satisfying the engine range.

Boolean refresh dedupe options are removed

Action required
breakinghigh impact

The deprecated boolean overload for refresh dedupe no longer works: true maps to cancel and false maps to defer.

Beforetypescript
await refresh({ dedupe: true })
await refresh({ dedupe: false })
Aftertypescript
await refresh({ dedupe: 'cancel' })
await refresh({ dedupe: 'defer' })
Migration

Use the explicit string behavior and test concurrent refresh requests.

Unhead 2 removes legacy head properties

Action required
breakinghigh impact

Unhead 2 removes vmid, hid, children, body and Promise inputs. Legacy configuration can lose tags or fail type checks.

Beforetypescript
useHead({
  script: [{ src: '/analytics.js', body: true }],
  meta: [{ hid: 'description', name: 'description', content: 'Guide' }]
})
Aftertypescript
useHead({
  script: [{ src: '/analytics.js', tagPosition: 'bodyClose' }],
  meta: [{ name: 'description', content: 'Guide' }]
})
Migration

Use supported tag properties and tagPosition for body tags. Resolve promises before calling useHead.

Route name and path leave meta

Action required
breakinghigh impact

The duplicate route.meta.name/path fields are no longer populated by Nuxt.

Beforetypescript
const name = route.meta.name
const path = route.meta.path
Aftertypescript
const name = route.name
const path = route.path
Migration

Read route.name and route.path directly and review custom route extensions.

Prerender routes move to Nitro configuration

Action required
breakinghigh impact

Legacy top-level generate.routes/exclude options are removed.

Beforetypescript
export default defineNuxtConfig({
  generate: { routes: ['/about'], exclude: ['/admin'] }
})
Aftertypescript
export default defineNuxtConfig({
  nitro: { prerender: { routes: ['/about'], ignore: ['/admin'] } }
})
Migration

Move route lists to nitro.prerender.routes/ignore. Inspect generated pages, canonical URLs and sitemap links.

window.__NUXT__ is removed after hydration

Action required
breakinghigh impact

Code that reads the global payload after hydration can fail. Nuxt app payload remains available through the app API.

Beforetypescript
const data = window.__NUXT__.data
Aftertypescript
const data = useNuxtApp().payload.data
Migration

Use useNuxtApp().payload in Nuxt context. Review analytics/custom integration code accessing the global.

Migration Tools

Application files default to app/

migrationmedium impact

Nuxt 4 adopts an app directory for application code. server, public, modules and shared remain at project root. Existing layouts are auto-detected for backward compatibility.

Beforetext
pages/
components/
composables/
server/
public/
Aftertext
app/pages/
app/components/
app/composables/
server/
public/
shared/
Migration

Review directory detection and custom aliases/tooling. If adopting app/, move application files and remember ~ points to app while ~~ points to project root.

Behavior Changes

Fetched data is shallow by default

Action required
behaviormedium impact

useAsyncData/useFetch return shallow data refs by default. Nested mutations do not trigger reactive updates as they did with deep refs.

Beforetypescript
const { data } = await useFetch('/api/profile')
data.value.preferences.theme = 'dark'
Aftertypescript
const { data } = await useFetch('/api/profile', { deep: true })
if (data.value) data.value.preferences.theme = 'dark'
Migration

Prefer immutable replacement; set deep: true per call when nested mutations are intentional and test dependent views.

Same data keys share state and consistent options

Action required
behaviormedium impact

Calls with the same useAsyncData key share data/error/status. Handler, deep, transform, pick, getCachedData and default must be consistent.

Beforetypescript
useAsyncData('users', () => $fetch('/api/users'), { deep: true })
useAsyncData('users', () => $fetch('/api/users'), { deep: false })
Aftertypescript
export const useUsers = () => useAsyncData(
  'users', () => $fetch('/api/users'), { deep: false }
)
// All consumers call useUsers().
Migration

Consolidate a key's options in one composable. Use distinct keys for genuinely different resources/options.

getCachedData also runs during refreshes

Action required
behaviormedium impact

Custom cached-data handlers are invoked for refresh/watch as well as initial fetch. Ignoring the cause can prevent an intended network refresh.

Beforetypescript
getCachedData: key => cache[key]
Aftertypescript
getCachedData: (key, nuxtApp, ctx) => {
  if (ctx.cause === 'refresh:manual' || ctx.cause === 'refresh:hook') return undefined
  return cache[key]
}
Migration

Use the context cause to bypass cached data on explicit refresh and review watch-triggered caching.

Initial data and error use undefined

Action required
behaviormedium impact

The initial empty value changes from null to undefined. Strict null-only checks can fail to recognize unloaded data.

Beforetypescript
if (data.value === null) {
  showPlaceholder()
}
Aftertypescript
if (data.value == null) {
  showPlaceholder()
}
Migration

Review initial defaults and checks; use nullish checks when both null and undefined represent an empty state.

pending reflects active request status

Action required
behaviormedium impact

pending is derived from status === pending. With immediate: false it starts false rather than treating idle as pending.

Beforevue
<script setup>
const { pending } = await useFetch('/api/items', { immediate: false })
</script>
<template><p v-if="pending">Not loaded yet</p></template>
Aftervue
<script setup>
const { status, execute } = await useFetch('/api/items', { immediate: false })
</script>
<template>
  <button v-if="status === 'idle'" @click="execute()">Load</button>
  <p v-if="status === 'pending'">Loading</p>
</template>
Migration

Use status to distinguish idle, pending, success and error; explicitly execute deferred requests.

clear restores the configured default

Action required
behaviormedium impact

Clearing async data now restores its default factory value instead of always setting data to undefined.

Beforetypescript
const { data, clear } = await useFetch('/api/items', { default: () => [] })
clear()
// Previously data reset to undefined.
Aftertypescript
const { data, clear } = await useFetch('/api/items', { default: () => [] })
clear()
// data is [] again; test length for an empty list.
Migration

Update reset logic and placeholder checks if relying on clear to remove a default value.

Prerendered data is shared across pages

Action required
behaviormedium impact

Payload data may be reused across prerendered pages. A static key for route-dependent content can produce the wrong page data.

Beforetypescript
const route = useRoute()
const { data } = await useAsyncData('article', () =>
  $fetch('/api/articles/' + route.params.slug)
)
Aftertypescript
const route = useRoute()
const key = computed(() => 'article:' + route.params.slug)
const { data } = await useAsyncData(key, () =>
  $fetch('/api/articles/' + route.params.slug)
)
Migration

Make keys identify the resource, including relevant route parameters, and verify prerendered HTML and payloads.

Error data is already parsed

Action required
behaviormedium impact

Nuxt error data is parsed rather than exposed as a JSON string. An extra JSON.parse can fail on error pages.

Beforetypescript
const details = JSON.parse(error.data)
Aftertypescript
const details = error.data
// Validate details before reading properties.
Migration

Consume error.data directly and validate its shape before rendering.

Override scanned page metadata in pages:resolved

Action required
behaviormedium impact

Nuxt scans page metadata after pages:extend. Metadata declared in pages may overwrite earlier hook changes.

Beforetypescript
export default defineNuxtConfig({
  hooks: { 'pages:extend': pages => {
    for (const page of pages) page.meta = { ...page.meta, layout: 'admin' }
  } }
})
Aftertypescript
export default defineNuxtConfig({
  hooks: { 'pages:resolved': pages => {
    for (const page of pages) page.meta = { ...page.meta, layout: 'admin' }
  } }
})
Migration

Use pages:resolved for overrides that must win over definePageMeta and test final routes.