--- title: Upgrade Guide description: 'Learn how to upgrade to the latest Nuxt version.' navigation.icon: i-ph-arrow-circle-up-duotone --- ## Upgrading Nuxt ### Latest release To upgrade Nuxt to the [latest release](https://github.com/nuxt/nuxt/releases), use the `nuxi upgrade` command. ::code-group ```bash [npm] npx nuxi upgrade ``` ```bash [yarn] yarn dlx nuxi upgrade ``` ```bash [pnpm] pnpm dlx nuxi upgrade ``` ```bash [bun] bunx nuxi upgrade ``` :: ### Nightly Release Channel To use the latest Nuxt build and test features before their release, read about the [nightly release channel](/docs/guide/going-further/nightly-release-channel) guide. ## Testing Nuxt 4 Nuxt 4 is planned to be released **on or before June 14** (though obviously this is dependent on having enough time after Nitro's major release to be properly tested in the community, so be aware that this is not an exact date). Until then, it is possible to test many of Nuxt 4's breaking changes from Nuxt version 3.12 or via the nightly release channel. ::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=r4wFKlcJK6c" target="_blank"} Watch a video from Alexander Lichter showing how to opt in to Nuxt 4's breaking changes already. :: ### Opting in to Nuxt 4 First, opt in to the nightly release channel [following these steps](/docs/guide/going-further/nightly-release-channel#opting-in). Then you can set your `compatibilityVersion` to match Nuxt 4 behavior: ```ts twoslash [nuxt.config.ts] export default defineNuxtConfig({ future: { compatibilityVersion: 4, }, // To re-enable _all_ Nuxt v3 behavior, set the following options: // srcDir: '.', // dir: { // app: 'app' // }, // experimental: { // sharedPrerenderData: false, // compileTemplate: true, // resetAsyncDataToUndefined: true, // templateUtils: true, // relativeWatchPaths: true, // defaults: { // useAsyncData: { // deep: true // } // } // }, // unhead: { // renderSSRHeadOptions: { // omitLineBreaks: false // } // } }) ``` When you set your `compatibilityVersion` to `4`, defaults throughout your Nuxt configuration will change to opt in to Nuxt v4 behavior, but you can granularly re-enable Nuxt v3 behavior when testing, following the commented out lines above. Please file issues if so, so that we can address them in Nuxt or in the ecosystem. ### Migrating to Nuxt 4 Breaking or significant changes will be noted here along with migration steps for backward/forward compatibility. ::alert This section is subject to change until the final release, so please check back here regularly if you are testing Nuxt 4 using `compatibilityVersion: 4`. :: #### New Directory Structure 🚦 **Impact Level**: Significant Nuxt now defaults to a new directory structure, with backwards compatibility (so if Nuxt detects you are using the old structure, such as with a top-level `pages/` directory, this new structure will not apply). 👉 [See full RFC](https://github.com/nuxt/nuxt/issues/26444) ##### What Changed * the new Nuxt default `srcDir` is `app/` by default, and most things are resolved from there. * `serverDir` now defaults to `/server` rather than `/server` * `modules` and `public` are resolved relative to `` by default * a new `dir.app` is added, which is the directory we look for `router.options.ts` and `spa-loading-template.html` - this defaults to `/`
An example v4 folder structure. ```sh .output/ .nuxt/ app/ assets/ components/ composables/ layouts/ middleware/ pages/ plugins/ utils/ app.config.ts app.vue router.options.ts modules/ node_modules/ public/ server/ api/ middleware/ plugins/ routes/ utils/ nuxt.config.ts ```
👉 For more details, see the [PR implementing this change](https://github.com/nuxt/nuxt/pull/27029). ##### Reasons for Change 1. **Performance** - placing all your code in the root of your repo causes issues with `.git/` and `node_modules/` folders being scanned/included by FS watchers which can significantly delay startup on non-Mac OSes. 1. **IDE type-safety** - `server/` and the rest of your app are running in two entirely different contexts with different global imports available, and making sure `server/` isn't _inside_ the same folder as the rest of your app is a big first step to ensuring you get good auto-completes in your IDE. ##### Migration Steps 1. Create a new directory called `app/`. 1. Move your `assets/`, `components/`, `composables/`, `layouts/`, `middleware/`, `pages/`, `plugins/` and `utils/` folders under it, as well as `app.vue`, `error.vue`, `app.config.ts`. If you have an `app/router-options.ts` or `app/spa-loading-template.html`, these paths remain the same. 1. Make sure your `nuxt.config.ts`, `modules/`, `public/` and `server/` folders remain outside the `app/` folder, in the root of your project. However, migration is _not required_. If you wish to keep your current folder structure, Nuxt should auto-detect it. (If it does not, please raise an issue.) The one exception is that if you _already_ have a custom `srcDir`. In this case, you should be aware that your `modules/`, `public/` and `server/` folders will be resolved from your `rootDir` rather than from your custom `srcDir`. You can override this by configuring `dir.modules`, `dir.public` and `serverDir` if you need to. You can also force a v3 folder structure with the following configuration: ```ts [nuxt.config.ts] export default defineNuxtConfig({ // This reverts the new srcDir default from `app` back to your root directory srcDir: '.', // This specifies the directory prefix for `app/router.options.ts` and `app/spa-loading-template.html` dir: { app: 'app' } }) ``` #### Shared Prerender Data 🚦 **Impact Level**: Medium ##### What Changed We enabled a previously experimental feature to share data from `useAsyncData` and `useFetch` calls, across different pages. See [original PR](https://github.com/nuxt/nuxt/pull/24894). ##### Reasons for Change This feature automatically shares payload _data_ between pages that are prerendered. This can result in a significant performance improvement when prerendering sites that use `useAsyncData` or `useFetch` and fetch the same data in different pages. For example, if your site requires a `useFetch` call for every page (for example, to get navigation data for a menu, or site settings from a CMS), this data would only be fetched once when prerendering the first page that uses it, and then cached for use when prerendering other pages. ##### Migration Steps Make sure that any unique key of your data is always resolvable to the same data. For example, if you are using `useAsyncData` to fetch data related to a particular page, you should provide a key that uniquely matches that data. (`useFetch` should do this automatically for you.) ```ts [app/pages/test/[slug\\].vue] // This would be unsafe in a dynamic page (e.g. `[slug].vue`) because the route slug makes a difference // to the data fetched, but Nuxt can't know that because it's not reflected in the key. const route = useRoute() const { data } = await useAsyncData(async () => { return await $fetch(`/api/my-page/${route.params.slug}`) }) // Instead, you should use a key that uniquely identifies the data fetched. const { data } = await useAsyncData(route.params.slug, async () => { return await $fetch(`/api/my-page/${route.params.slug}`) }) ``` Alternatively, you can disable this feature with: ```ts twoslash [nuxt.config.ts] export default defineNuxtConfig({ experimental: { sharedPrerenderData: false } }) ``` #### Default `data` and `error` values in `useAsyncData` and `useFetch` 🚦 **Impact Level**: Minimal ##### What Changed `data` and `error` objects returned from `useAsyncData` will now default to `undefined`. ##### Reasons for Change Previously `data` was initialized to `null` but reset in `clearNuxtData` to `undefined`. `error` was initialized to `null`. This change is to bring greater consistency. ##### Migration Steps If you encounter any issues you can revert back to the previous behavior with: ```ts twoslash [nuxt.config.ts] // @errors: 2353 export default defineNuxtConfig({ experimental: { defaults: { useAsyncData: { value: 'null', errorValue: 'null' } } } }) ``` Please report an issue if you are doing this, as we do not plan to keep this as configurable. #### Removal of deprecated `boolean` values for `dedupe` option when calling `refresh` in `useAsyncData` and `useFetch` 🚦 **Impact Level**: Minimal ##### What Changed Previously it was possible to pass `dedupe: boolean` to `refresh`. These were aliases of `cancel` (`true`) and `defer` (`false`). ```ts twoslash [app.vue] // @errors: 2322 const { refresh } = await useAsyncData(async () => ({ message: 'Hello, Nuxt 3!' })) async function refreshData () { await refresh({ dedupe: true }) } ``` ##### Reasons for Change These aliases were removed, for greater clarity. The issue came up when adding `dedupe` as an option to `useAsyncData`, and we removed the boolean values as they ended up being _opposites_. `refresh({ dedupe: false })` meant 'do not _cancel_ existing requests in favour of this new one'. But passing `dedupe: true` within the options of `useAsyncData` means 'do not make any new requests if there is an existing pending request.' (See [PR](https://github.com/nuxt/nuxt/pull/24564#pullrequestreview-1764584361).) ##### Migration Steps The migration should be straightforward: ```diff const { refresh } = await useAsyncData(async () => ({ message: 'Hello, Nuxt 3!' })) async function refreshData () { - await refresh({ dedupe: true }) + await refresh({ dedupe: 'cancel' }) - await refresh({ dedupe: false }) + await refresh({ dedupe: 'defer' }) } ``` #### Respect defaults when clearing `data` in `useAsyncData` and `useFetch` 🚦 **Impact Level**: Minimal ##### What Changed If you provide a custom `default` value for `useAsyncData`, this will now be used when calling `clear` or `clearNuxtData` and it will be reset to its default value rather than simply unset. ##### Reasons for Change Often users set an appropriately empty value, such as an empty array, to avoid the need to check for `null`/`undefined` when iterating over it. This should be respected when resetting/clearing the data. ##### Migration Steps If you encounter any issues you can revert back to the previous behavior, for now, with: ```ts twoslash [nuxt.config.ts] // @errors: 2353 export default defineNuxtConfig({ experimental: { resetAsyncDataToUndefined: true, } }) ``` Please report an issue if you are doing so, as we do not plan to keep this as configurable. #### Shallow Data Reactivity in `useAsyncData` and `useFetch` 🚦 **Impact Level**: Minimal The `data` object returned from `useAsyncData`, `useFetch`, `useLazyAsyncData` and `useLazyFetch` is now a `shallowRef` rather than a `ref`. ##### What Changed When new data is fetched, anything depending on `data` will still be reactive because the entire object is replaced. But if your code changes a property _within_ that data structure, this will not trigger any reactivity in your app. ##### Reasons for Change This brings a **significant** performance improvement for deeply nested objects and arrays because Vue does not need to watch every single property/array for modification. In most cases, `data` should also be immutable. ##### Migration Steps In most cases, no migration steps are required, but if you rely on the reactivity of the data object then you have two options: 1. You can granularly opt in to deep reactivity on a per-composable basis: ```diff - const { data } = useFetch('/api/test') + const { data } = useFetch('/api/test', { deep: true }) ``` 1. You can change the default behavior on a project-wide basis (not recommended): ```ts twoslash [nuxt.config.ts] export default defineNuxtConfig({ experimental: { defaults: { useAsyncData: { deep: true } } } }) ``` #### Absolute Watch Paths in `builder:watch` 🚦 **Impact Level**: Minimal ##### What Changed The Nuxt `builder:watch` hook now emits a path which is absolute rather than relative to your project `srcDir`. ##### Reasons for Change This allows us to support watching paths which are outside your `srcDir`, and offers better support for layers and other more complex patterns. ##### Migration Steps We have already proactively migrated the public Nuxt modules which we are aware use this hook. See [issue #25339](https://github.com/nuxt/nuxt/issues/25339). However, if you are a module author using the `builder:watch` hook and wishing to remain backwards/forwards compatible, you can use the following code to ensure that your code works the same in both Nuxt v3 and Nuxt v4: ```diff + import { relative, resolve } from 'node:fs' // ... nuxt.hook('builder:watch', async (event, path) => { + path = relative(nuxt.options.srcDir, resolve(nuxt.options.srcDir, path)) // ... }) ``` #### Removal of `window.__NUXT__` object ##### What Changed We are removing the global `window.__NUXT__` object after the app finishes hydration. ##### Reasons for Change This opens the way to multi-app patterns ([#21635](https://github.com/nuxt/nuxt/issues/21635)) and enables us to focus on a single way to access Nuxt app data - `useNuxtApp()`. ##### Migration Steps The data is still available, but can be accessed with `useNuxtApp().payload`: ```diff - console.log(window.__NUXT__) + console.log(useNuxtApp().payload) ``` #### Directory index scanning 🚦 **Impact Level**: Medium ##### What Changed Child folders in your `middleware/` folder are also scanned for `index` files and these are now also registered as middleware in your project. ##### Reasons for Change Nuxt scans a number of folders automatically, including `middleware/` and `plugins/`. Child folders in your `plugins/` folder are scanned for `index` files and we wanted to make this behavior consistent between scanned directories. ##### Migration Steps Probably no migration is necessary but if you wish to revert to previous behavior you can add a hook to filter out these middleware: ```ts export default defineNuxtConfig({ hooks: { 'app:resolve'(app) { app.middleware = app.middleware.filter(mw => !/\/index\.[^/]+$/.test(mw.path)) } } }) ``` #### Template Compilation Changes 🚦 **Impact Level**: Minimal ##### What Changed Previously, Nuxt used `lodash/template` to compile templates located on the file system using the `.ejs` file format/syntax. In addition, we provided some template utilities (`serialize`, `importName`, `importSources`) which could be used for code-generation within these templates, which are now being removed. ##### Reasons for Change In Nuxt v3 we moved to a 'virtual' syntax with a `getContents()` function which is much more flexible and performant. In addition, `lodash/template` has had a succession of security issues. These do not really apply to Nuxt projects because it is being used at build-time, not runtime, and by trusted code. However, they still appear in security audits. Moreover, `lodash` is a hefty dependency and is unused by most projects. Finally, providing code serialization functions directly within Nuxt is not ideal. Instead, we maintain projects like [unjs/knitwork](http://github.com/unjs/knitwork) which can be dependencies of your project, and where security issues can be reported/resolved directly without requiring an upgrade of Nuxt itself. ##### Migration Steps We have raised PRs to update modules using EJS syntax, but if you need to do this yourself, you have three backwards/forwards-compatible alternatives: * Moving your string interpolation logic directly into `getContents()`. * Using a custom function to handle the replacement, such as in https://github.com/nuxt-modules/color-mode/pull/240. * Continuing to use `lodash`, as a dependency of _your_ project rather than Nuxt: ```diff + import { readFileSync } from 'node:fs' + import { template } from 'lodash-es' // ... addTemplate({ fileName: 'appinsights-vue.js' options: { /* some options */ }, - src: resolver.resolve('./runtime/plugin.ejs'), + getContents({ options }) { + const contents = readFileSync(resolver.resolve('./runtime/plugin.ejs'), 'utf-8') + return template(contents)({ options }) + }, }) ``` Finally, if you are using the template utilities (`serialize`, `importName`, `importSources`), you can replace them as follows with utilities from `knitwork`: ```ts import { genDynamicImport, genImport, genSafeVariableName } from 'knitwork' const serialize = (data: any) => JSON.stringify(data, null, 2).replace(/"{(.+)}"(?=,?$)/gm, r => JSON.parse(r).replace(/^{(.*)}$/, '$1')) const importSources = (sources: string | string[], { lazy = false } = {}) => { return toArray(sources).map((src) => { if (lazy) { return `const ${genSafeVariableName(src)} = ${genDynamicImport(src, { comment: `webpackChunkName: ${JSON.stringify(src)}` })}` } return genImport(src, genSafeVariableName(src)) }).join('\n') } const importName = genSafeVariableName ``` #### Removal of Experimental Features 🚦 **Impact Level**: Minimal ##### What Changed Four experimental features are no longer configurable in Nuxt 4: * `experimental.treeshakeClientOnly` will be `true` (default since v3.0) * `experimental.configSchema` will be `true` (default since v3.3) * `experimental.polyfillVueUseHead` will be `false` (default since v3.4) * `experimental.respectNoSSRHeader` will be `false` (default since v3.4) * `vite.devBundler` is no longer configurable - it will use `vite-node` by default ##### Reasons for Change These options have been set to their current values for some time and we do not have a reason to believe that they need to remain configurable. ##### Migration Steps * `polyfillVueUseHead` is implementable in user-land with [this plugin](https://github.com/nuxt/nuxt/blob/f209158352b09d1986aa320e29ff36353b91c358/packages/nuxt/src/head/runtime/plugins/vueuse-head-polyfill.ts#L10-L11) * `respectNoSSRHeader`is implementable in user-land with [server middleware](https://github.com/nuxt/nuxt/blob/c660b39447f0d5b8790c0826092638d321cd6821/packages/nuxt/src/core/runtime/nitro/no-ssr.ts#L8-L9) ## Nuxt 2 vs Nuxt 3 In the table below, there is a quick comparison between 3 versions of Nuxt: Feature / Version | Nuxt 2 | Nuxt Bridge | Nuxt 3 -------------------------|-----------------|------------------|--------- Vue | 2 | 2 | 3 Stability | 😊 Stable | 😊 Stable | 😊 Stable Performance | 🏎 Fast | ✈️ Faster | 🚀 Fastest Nitro Engine | ❌ | ✅ | ✅ ESM support | 🌙 Partial | 👍 Better | ✅ TypeScript | ☑️ Opt-in | 🚧 Partial | ✅ Composition API | ❌ | 🚧 Partial | ✅ Options API | ✅ | ✅ | ✅ Components Auto Import | ✅ | ✅ | ✅ `