| Category | Highlights |
|---|---|
| New Features |
New app/ directory as the default srcDir; new shared/ directory with auto-imports for code shared between the Vue app and Nitro server; reactive key support in useAsyncData and useFetch; socket-based CLI-to-Vite communication; Node.js v8 compile cache; native fs.watch file watching
|
| Improvements | Singleton data-fetching layer: same-key calls now share refs and auto-clean up on unmount; normalized component names align auto-import names with Vue DevTools names; separate TypeScript projects per context (app, server, shared, config); corrected module loading order in layers (layer modules before project modules); Unhead v2 with Capo.js tag sorting |
| Breaking Changes |
Default srcDir changed to app/; server/ and public/ now resolve from rootDir not srcDir; same-key useAsyncData calls must not have conflicting options; getCachedData now receives a ctx object with a cause field; route metadata deduplication removes duplicate fields from route.meta; vmid and hid props removed from Unhead
|
| Deprecations |
Nuxt 2 compatibility removed from @nuxt/kit; legacy utilities and deprecated APIs cleaned up; Promise input to Unhead tags no longer supported; children and body props removed from Unhead
|
What Is the New Directory Structure in Nuxt 4?
Nuxt 4 introduces a new default project layout where all application source code lives inside an app/ subdirectory, separating it cleanly from configuration files, the server/ folder, and tooling at the project root.
In practice, the ~ alias now points to app/ instead of the project root. Your components/, pages/, layouts/, composables/, plugins/, and utils/ all move under app/. Meanwhile server/, public/, content/, layers/, modules/, and nuxt.config.ts stay in the root.
This matters if you are on Windows or Linux: keeping application code in its own subdirectory dramatically reduces the number of paths that file-system watchers have to monitor. The .git/ and node_modules/ folders no longer sit alongside your source files, which cuts cold-start and rebuild times noticeably on those OSes.
A key addition is the new shared/ directory at the root level. Code placed in shared/utils/ and shared/types/ is auto-imported and available in both the Vue application and the Nitro server, giving you a clean, typed bridge between the two runtimes without duplication.
my-nuxt-app/
├─ app/
│ ├─ assets/
│ ├─ components/
│ ├─ composables/
│ ├─ layouts/
│ ├─ middleware/
│ ├─ pages/
│ ├─ plugins/
│ ├─ utils/
│ ├─ app.vue
│ ├─ app.config.ts
│ └─ error.vue
├─ content/
├─ public/
├─ shared/
│ ├─ types/
│ └─ utils/
├─ server/
└─ nuxt.config.ts
Watch out for one edge case: if you already defined a custom srcDir in Nuxt 3, the migration is slightly different. Your modules/, public/, shared/, and server/ directories will now be resolved from rootDir rather than your custom srcDir. You can override this with dir.modules, dir.public, and serverDir in nuxt.config.ts if needed.
Migration is not required. If Nuxt detects a top-level pages/ or components/ folder, it automatically keeps the Nuxt 3 structure. You can also pin the old layout explicitly:
// nuxt.config.ts - opt out of the new srcDir
export default defineNuxtConfig({
srcDir: '.',
dir: {
app: 'app',
},
})
The Codemod team has published an automated migration script to handle the file moves:
npx codemod@latest nuxt/4/migration-recipe
How Does Data Fetching Work Differently in Nuxt 4?
Nuxt 4 overhauls the data-fetching layer so that all useAsyncData and useFetch calls sharing the same key now operate on a single set of shared data, error, and status refs, and automatically dispose of that data when the last consuming component unmounts.
This is the change most likely to require code review in existing applications. If two components call useAsyncData('users', ...) with different options — for example one with deep: false and one with deep: true — Nuxt 4 will emit a warning. The recommendation is to encapsulate shared keys in a dedicated composable:
// app/composables/useUserData.ts
export function useUserData(userId: string) {
return useAsyncData(
`user-${userId}`,
() => fetchUser(userId),
{
deep: true,
transform: (user) => ({ ...user, lastAccessed: new Date() }),
},
)
}
The getCachedData option has also changed in a meaningful way. It is now called on every fetch trigger — including watcher-driven refetches and manual refreshNuxtData() calls. It also receives a ctx object with a cause field so you can decide per-trigger whether to serve cached data or go to the network:
useAsyncData('key', fetchFunction, {
getCachedData: (key, nuxtApp, ctx) => {
// ctx.cause: 'initial' | 'refresh:hook' | 'refresh:manual' | 'watch'
if (ctx.cause === 'refresh:manual') return undefined // force network on manual refresh
return cachedData[key]
},
})
Reactive keys are now natively supported. You can pass a computed ref, a plain ref, or a getter function as the key, and the composable will automatically refetch when the key changes:
const userId = ref('abc')
const { data } = useAsyncData(() => `user-${userId.value}`, () => fetchUser(userId.value))
In practice, the automatic cleanup on unmount prevents the gradual memory growth that large SPAs with many useAsyncData calls used to experience. Most teams will welcome this change, but review any pattern where you expected stale data to persist after a component unmounts.
How Does TypeScript Support Improve in Nuxt 4?
Nuxt 4 generates separate TypeScript projects for each runtime context — app code, server code, the shared/ folder, and builder configuration — which means your IDE can finally give accurate autocompletion and type inference without mixing server-only and client-only globals.
Previously, having server/ inside or adjacent to app/ caused TypeScript to bleed types across contexts. You might get false autocompletion for Node.js APIs in a Vue component, or miss type errors because the server globals masked them. With the new setup, these contexts are truly isolated.
You now only need one tsconfig.json in your project root. Nuxt generates the rest automatically under .nuxt/. This is the change most likely to surface previously hidden type errors. Watch out for:
- Server utilities accidentally imported into client code (now caught at compile time)
- Missing types in
shared/that were previously inferred from the wrong context - Third-party module type declarations that assumed the old monolithic tsconfig structure
Most teams will find the initial upgrade surfaces a handful of real bugs alongside some legitimate type narrowing noise from the new context separation. Treat both as signal — the former are real bugs, and the latter can usually be resolved by moving utilities to the correct context folder.
What Performance Improvements Come with the Nuxt 4 CLI?
The @nuxt/cli in Nuxt 4 delivers noticeably faster development server startup and day-to-day rebuild times through a combination of four targeted improvements working together.
- Socket-based communication: The CLI and Vite dev server now talk through internal Unix sockets instead of a network TCP port. This removes port-reservation overhead and is particularly impactful on Windows, where loopback networking has historically been slow.
- Node.js v8 compile cache: Nuxt now leverages Node's built-in compile cache so that the JavaScript parsed during one cold start is reused on the next. Subsequent starts are measurably faster on the same machine.
- Native file watching: The CLI switches from polling-based file watchers to Node's
fs.watchAPIs. This cuts CPU usage during development and makes watcher wake-up times faster, especially on Linux. - New directory structure synergy: Placing all source code under
app/reduces the number of paths that the file watcher has to monitor, compounding thefs.watchimprovement.
In practice, teams working on large monorepos or on Windows will notice the most dramatic difference. The upgrade guide recommends running npx nuxt upgrade --dedupe rather than a plain npm install, because the --dedupe flag also cleans up the lockfile and pulls in updated versions of unjs ecosystem packages that Nuxt depends on.
What Breaking Changes Should Module Authors Know About in Nuxt 4?
Module authors face the most consequential set of changes in Nuxt 4: Nuxt 2 compatibility has been completely removed from @nuxt/kit, component naming conventions have changed, module loading order in layers has been corrected, and Unhead v2 removes several low-level APIs.
Component names are now normalized. A component at components/SomeFolder/MyComponent.vue previously had the Vue-internal name MyComponent (used for <KeepAlive> and Vue DevTools) but required SomeFolderMyComponent for Nuxt auto-import. In Nuxt 4 both names are unified. This is a silent runtime change: tests using findComponent from @vue/test-utils and any <KeepAlive name="..."> bindings that used the short name must be updated.
Layer module loading order is corrected. Previously, project-level modules were loaded before layer modules — the reverse of the intuitive precedence. Nuxt 4 loads layer modules first, then project modules. Most projects benefit from this, but if a module was written to work around the old incorrect order, it may need to be rethought. The modules:done hook is the right escape hatch for modules that must run after all others.
Unhead v2 removes legacy props. The vmid, hid, children, and body properties on head tag objects are gone. Promise inputs to useHead are no longer accepted. Tags are now ordered using Capo.js by default, which produces better-performing <head> output but may change the tag order you relied on for testing:
// Before (Nuxt 3)
useHead({
meta: [{ name: 'description', content: 'My App', vmid: 'description' }],
})
// After (Nuxt 4)
useHead({
meta: [{ name: 'description', content: 'My App' }],
})
Route metadata deduplication. Fields like name and path that were previously accessible on both route.name and route.meta.name are now only on the route object itself. Update any code that reads from route.meta.name to use route.name directly.
Frequently Asked Questions about Nuxt 4
Do I have to migrate my project to the new app/ directory structure in Nuxt 4?
No, migration is optional. Nuxt 4 auto-detects your existing structure and continues to work exactly as before if it finds top-level directories like pages/ or components/ in your project root. You only need to migrate if you want the improved file-watcher performance and IDE type-safety that come with the new layout.
Will useAsyncData calls with the same key break after upgrading to Nuxt 4?
They will not break outright, but Nuxt 4 will emit a warning if two calls share the same explicit key while having conflicting options such as different deep, transform, or pick values. The fix is to extract those calls into a shared composable with a consistent set of options, for example exporting a useUserData function from a composables file that centralizes the key and its configuration.
How do I upgrade to Nuxt 4 without manually migrating every file?
Run npx codemod@latest nuxt/4/migration-recipe to execute the official automated migration scripts, then run npx nuxt upgrade --dedupe to install the new version and deduplicate your lockfile. The upgrade guide recommends reading through the migration documentation before running either command so you understand which areas of your project may be affected.
Does Nuxt 4 remove support for Nuxt 2 modules?
Yes. Nuxt 2 compatibility has been fully removed from @nuxt/kit in Nuxt 4. End-users of applications are unlikely to notice this directly, but maintainers of community modules that still used @nuxt/kit Nuxt 2 compatibility utilities will need to update their modules before they work correctly with Nuxt 4.
Why do component names behave differently in Nuxt 4 with KeepAlive and Vue DevTools?
Nuxt 4 normalizes the Vue-internal component name to match the Nuxt auto-import name, so a component at components/SomeFolder/MyComponent.vue is now consistently called SomeFolderMyComponent in both contexts. Previously the internal Vue name was just MyComponent, which meant KeepAlive bindings and Vue DevTools showed a different name than what you used to import the component. Update any KeepAlive name attributes and findComponent calls in tests to use the full prefixed name.
Can I test Nuxt 5 features while running Nuxt 4?
Yes. From Nuxt 4.2 onward you can set future.compatibilityVersion to 5 in your nuxt.config.ts to opt in to Nuxt 5 behaviors including the Vite Environment API, normalized page component names, comment node placeholders for client-only components, and a non-async callHook that is 20 to 40 times faster than the previous Promise-wrapping implementation.