Upgrade Guide
Learn how to upgrade to the latest Nuxt version.
Upgrading Nuxt
Latest release
To upgrade Nuxt to the latest release, use the nuxi upgrade
command.
npx nuxi upgrade
Nightly Release Channel
To use the latest Nuxt build and test features before their release, read about the 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 on the nightly release channel.
Opting in to Nuxt 4
First, opt in to the nightly release channel following these steps.
Then you can set your compatibilityVersion
to match Nuxt 4 behavior:
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,
// 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.
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
What Changed
- the new Nuxt default
srcDir
isapp/
by default, and most things are resolved from there. serverDir
now defaults to<rootDir>/server
rather than<srcDir>/server
modules
andpublic
are resolved relative to<rootDir>
by default- a new
dir.app
is added, which is the directory we look forrouter.options.ts
andspa-loading-template.html
- this defaults to<srcDir>/
An example v4 folder structure.
.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.
Reasons for Change
- Performance - placing all your code in the root of your repo causes issues with
.git/
andnode_modules/
folders being scanned/included by FS watchers which can significantly delay startup on non-Mac OSes. - IDE type-safety -
server/
and the rest of your app are running in two entirely different contexts with different global imports available, and making sureserver/
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
- Create a new directory called
app/
. - Move your
assets/
,components/
,composables/
,layouts/
,middleware/
,pages/
,plugins/
andutils/
folders under it, as well asapp.vue
,error.vue
,app.config.ts
. If you have anapp/router-options.ts
orapp/spa-loading-template.html
, these paths remain the same. - Make sure your
nuxt.config.ts
,modules/
,public/
andserver/
folders remain outside theapp/
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.) You can also force a v3 folder structure with the following configuration:
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.
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.)
// 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:
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:
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.
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:
- You can granularly opt in to deep reactivity on a per-composable basis:
- const { data } = useFetch('/api/test') + const { data } = useFetch('/api/test', { deep: true })
- You can change the default behavior on a project-wide basis (not recommended):
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.
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:
+ import { relative, resolve } from 'node:fs'
// ...
nuxt.hook('builder:watch', async (event, path) => {
+ path = relative(nuxt.options.srcDir, resolve(nuxt.options.srcDir, path))
// ...
})
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:
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 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:
+ 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
:
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:
treeshakeClientOnly
will betrue
(default since v3.0)configSchema
will betrue
(default since v3.3)polyfillVueUseHead
will befalse
(default since v3.4)respectNoSSRHeader
will befalse
(default since v3.4)
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 pluginrespectNoSSRHeader
is implementable in user-land with server middleware
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 | โ | โ | โ |
<script setup> syntax | โ | ๐ง Partial | โ |
Auto Imports | โ | โ | โ |
webpack | 4 | 4 | 5 |
Vite | โ ๏ธ Partial | ๐ง Partial | โ |
Nuxi CLI | โ Old | โ nuxi | โ nuxi |
Static sites | โ | โ | โ |
Nuxt 2 to Nuxt 3
The migration guide provides a step-by-step comparison of Nuxt 2 features to Nuxt 3 features and guidance to adapt your current application.
Nuxt 2 to Nuxt Bridge
If you prefer to progressively migrate your Nuxt 2 application to Nuxt 3, you can use Nuxt Bridge. Nuxt Bridge is a compatibility layer that allows you to use Nuxt 3 features in Nuxt 2 with an opt-in mechanism.