Nuxt 4 in 2026: New Directory Structure and Migration from Nuxt 3
Complete guide to Nuxt 4 directory structure, migration from Nuxt 3, data fetching changes, and TypeScript improvements. Step-by-step tutorial with code examples.

Nuxt 4 introduces a redesigned directory structure that separates application code from configuration, along with a singleton data fetching layer, shallow reactivity defaults, and split TypeScript contexts. Released in July 2025 and now at version 4.4, this major release focuses on evolution rather than revolution — making the migration path from Nuxt 3 significantly smoother than the Nuxt 2 to 3 jump.
The Nuxt team partnered with Codemod to automate most migration steps. Run npx codemod@latest nuxt/4/migration-recipe to handle directory restructuring, data fetching updates, and deprecated API replacements automatically.
The New app/ Directory Structure in Nuxt 4
Nuxt 4 moves all application source code into an app/ directory by default. This separation solves a real problem: file watchers on Linux and Windows perform significantly better when application code lives in a dedicated subdirectory rather than mixed with node_modules/, .git/, and configuration files.
The new layout follows this structure:
my-nuxt-app/
├─ app/
│ ├─ assets/
│ ├─ components/
│ ├─ composables/
│ ├─ layouts/
│ ├─ middleware/
│ ├─ pages/
│ ├─ plugins/
│ ├─ utils/
│ ├─ app.vue
│ ├─ app.config.ts
│ └─ error.vue
├─ content/
├─ public/
├─ shared/ # New: code shared between app and server
├─ server/
└─ nuxt.config.tsThe shared/ directory is a notable addition. Any composable or utility placed in shared/ becomes auto-imported in both the Vue app and the Nitro server, eliminating the need for manual imports when sharing validation schemas, type definitions, or utility functions across contexts.
Step-by-Step Migration from Nuxt 3 to Nuxt 4
The upgrade process starts with a single command. Nuxt detects the existing flat structure and continues working without any changes, so the migration can happen incrementally.
# Upgrade Nuxt and deduplicate dependencies
npx nuxt upgrade --dedupeAfter upgrading the package, move application files into the app/ directory:
# Automate the directory restructuring
npx codemod@latest nuxt/4/file-structureThis codemod moves assets/, components/, composables/, layouts/, middleware/, pages/, plugins/, utils/, app.vue, error.vue, and app.config.ts into app/. Files that belong at the root — nuxt.config.ts, server/, public/, and content/ — stay in place.
For projects that need to delay the restructuring, set the source directory explicitly:
export default defineNuxtConfig({
srcDir: '.',
dir: { app: 'app' },
})This configuration tells Nuxt 4 to resolve files from the project root, matching the Nuxt 3 behavior exactly.
Singleton Data Fetching Layer and Reactive Keys
Nuxt 4 fundamentally changes how useAsyncData and useFetch manage data. Multiple components calling the same key now share a single reactive reference instead of maintaining independent copies.
export function useProductData(productId: string) {
return useAsyncData(
`product-${productId}`,
() => $fetch(`/api/products/${productId}`),
{
getCachedData: (key, nuxtApp, ctx) => {
// ctx.cause tells why the fetch is happening
if (ctx.cause === 'refresh:manual') return undefined
return nuxtApp.payload.data[key]
},
},
)
}Three changes stand out in this new data layer:
- Shared refs: calling
useProductData('abc')in two components returns the samedata,error, andstatusrefs. Updating one updates both. - Automatic cleanup: when the last component using a key unmounts, Nuxt frees the associated data from memory.
- Reactive keys: wrapping a key in a computed or ref triggers automatic refetching when the value changes.
The getCachedData callback now receives a context object with a cause property ('initial', 'refresh:hook', 'refresh:manual', or 'watch'), enabling fine-grained control over when to serve cached data versus fetching fresh data.
Data and error from useAsyncData/useFetch now default to undefined instead of null. Update any === null checks to === undefined or use a loose equality check.
Shallow Reactivity by Default for Better Performance
Nuxt 4 switches data from useAsyncData and useFetch to shallowRef instead of ref. Vue no longer recursively tracks every nested property, which delivers measurable performance improvements for API responses with deeply nested objects or large arrays.
<script setup lang="ts">
// Data is now a shallowRef by default
const { data: metrics } = await useFetch('/api/dashboard/metrics')
// Direct property mutation won't trigger reactivity
// metrics.value.visits = 100 // Won't trigger re-render
// Replace the entire value to trigger updates
metrics.value = { ...metrics.value, visits: 100 }
// Or opt into deep reactivity for this specific call
const { data: settings } = await useFetch('/api/settings', {
deep: true,
})
</script>For most read-only data displays (dashboards, product listings, article pages), shallow reactivity works without any code changes. The deep: true option remains available for forms or interactive UIs that mutate nested properties directly.
Ready to ace your Vue.js / Nuxt.js interviews?
Practice with our interactive simulators, flashcards, and technical tests.
TypeScript Context Splitting and Improved Type Safety
Nuxt 4 generates separate TypeScript configurations for each context in the project:
.nuxt/tsconfig.app.json— Vue application code.nuxt/tsconfig.server.json— Nitro server code.nuxt/tsconfig.shared.json— Shared utilities.nuxt/tsconfig.node.json— Build-time configuration
This separation means the IDE no longer suggests server-only APIs in client code, and vice versa. A single tsconfig.json at the project root references all four configs:
{
"files": [],
"references": [
{ "path": "./.nuxt/tsconfig.app.json" },
{ "path": "./.nuxt/tsconfig.server.json" },
{ "path": "./.nuxt/tsconfig.shared.json" },
{ "path": "./.nuxt/tsconfig.node.json" }
]
}Type checking in CI also changes. The vue-tsc command now requires the -b flag (build mode) to process project references correctly:
# Before (Nuxt 3)
nuxt prepare && vue-tsc --noEmit
# After (Nuxt 4)
nuxt prepare && vue-tsc -b --noEmitAnother TypeScript change: compilerOptions.noUncheckedIndexedAccess is true by default. Accessing an array element or object property by index now returns T | undefined, catching potential runtime errors at compile time.
Normalized Component Names and Vue Router v5
Nuxt 4.3 upgraded to Vue Router v5, removing the dependency on unplugin-vue-router. For most applications, this upgrade is transparent.
Component naming conventions are now standardized. A component at components/dashboard/MetricsCard.vue gets the name DashboardMetricsCard consistently across all contexts — including <KeepAlive>, Vue DevTools, and test utilities.
<!-- app/pages/dashboard.vue -->
<template>
<NuxtPage :keepalive="{
include: ['DashboardMetricsCard', 'DashboardRecentActivity'],
}" />
</template>Projects using <KeepAlive> with component name filters need to update the names to match this new convention. The previous behavior, where the name could vary depending on context, no longer applies.
Handling Breaking Changes in Head Management
Nuxt 4 ships with Unhead v2, which removes several deprecated properties from useHead and useSeoMeta:
<script setup lang="ts">
const route = useRoute()
const { data: product } = await useFetch(`/api/products/${route.params.id}`)
// Unhead v2: removed vmid, hid, children, body props
useSeoMeta({
title: () => product.value?.name ?? 'Product',
ogTitle: () => product.value?.name ?? 'Product',
description: () => product.value?.description ?? '',
ogImage: () => product.value?.imageUrl ?? '',
})
</script>For projects relying on template parameters or alias sorting, install these as explicit plugins:
import { TemplateParamsPlugin, AliasSortingPlugin } from '@unhead/vue/plugins'
export default defineNuxtPlugin({
setup() {
const unhead = injectHead()
unhead.use(TemplateParamsPlugin)
unhead.use(AliasSortingPlugin)
},
})Nuxt 3 continues to receive security updates and critical bug fixes until July 31, 2026. After that date, Nuxt 3 becomes unsupported. Planning the migration now avoids running production applications on an EOL framework.
Migration Checklist and Common Pitfalls
The automated codemod handles most changes, but several items require manual attention:
window.__NUXT__removal: replace withuseNuxtApp().payload. This global object is deleted after hydration in Nuxt 4.pages:extendhook: switch to the newpages:resolvedhook, which runs after page meta scanning.dedupeboolean: replacerefresh({ dedupe: true })withrefresh({ dedupe: 'cancel' })andfalsewith'defer'.- Inline styles: only Vue component styles are inlined by default; global CSS loads as separate files. Add
features: { inlineStyles: true }to restore the Nuxt 3 behavior. clearNuxtState: now resets to initial values instead ofundefined. UseclearNuxtState('key', { reset: false })for the old behavior.
A practical migration sequence for production applications:
- Run
npx nuxt upgrade --dedupeand verify the application builds - Run the codemod:
npx codemod@latest nuxt/4/migration-recipe - Move files to
app/(automated by the file-structure codemod) - Update
nullchecks toundefinedin data fetching logic - Test
<KeepAlive>components with normalized names - Update CI type checking to use
vue-tsc -b --noEmit - Run the full test suite and fix any TypeScript errors exposed by
noUncheckedIndexedAccess
For deeper coverage of Vue and Nuxt concepts, explore the Vue/Nuxt interview questions on SharpSkill, or review the SSR and static generation guide for background on rendering strategies that carry over to Nuxt 4.
Conclusion
- Nuxt 4.4 (current as of April 2026) stabilizes the
app/directory structure, singleton data fetching, and split TypeScript contexts as production-ready defaults - The
npx codemod@latest nuxt/4/migration-recipecommand automates directory restructuring, deprecated API replacements, and data fetching updates - Shallow reactivity via
shallowRefimproves performance for read-heavy pages without requiring code changes in most cases - Separate TypeScript configs per context (app, server, shared, node) eliminate cross-context type leaks and improve IDE autocompletion
- Nuxt 3 reaches end-of-life on July 31, 2026 — migrating before that date keeps applications on a supported, actively maintained version
- Vue Router v5 and Unhead v2 bring cleaner APIs at the cost of removing deprecated properties that should be audited during migration
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

Vue 3 Pinia vs Vuex: Modern State Management and Interview Questions 2026
Pinia vs Vuex compared in depth: API design, TypeScript support, performance, migration strategies, and common Vue state management interview questions for 2026.

Nuxt 3: SSR and Static Generation Complete Guide
Master SSR and static generation with Nuxt 3. From useFetch to route rules, learn how to optimize performance for your Vue.js applications.

Essential Vue.js Interview Questions: 25 Questions to Land the Job
Prepare for Vue.js interviews with these 25 essential questions. From reactivity to composables, master the key concepts to ace your next interview.