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 directory structure and migration guide from Nuxt 3

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.

Automated Migration Available

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:

text
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.ts

The 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.

bash
# Upgrade Nuxt and deduplicate dependencies
npx nuxt upgrade --dedupe

After upgrading the package, move application files into the app/ directory:

bash
# Automate the directory restructuring
npx codemod@latest nuxt/4/file-structure

This 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:

nuxt.config.tstypescript
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.

app/composables/useProductData.tstypescript
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 same data, error, and status refs. 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.

Default Values Changed

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.

app/pages/dashboard.vuetypescript
<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:

json
{
  "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:

bash
# Before (Nuxt 3)
nuxt prepare && vue-tsc --noEmit

# After (Nuxt 4)
nuxt prepare && vue-tsc -b --noEmit

Another 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.

vue
<!-- 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:

app/pages/product/[id].vuetypescript
<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:

app/plugins/unhead.tstypescript
import { TemplateParamsPlugin, AliasSortingPlugin } from '@unhead/vue/plugins'

export default defineNuxtPlugin({
  setup() {
    const unhead = injectHead()
    unhead.use(TemplateParamsPlugin)
    unhead.use(AliasSortingPlugin)
  },
})
Nuxt 3 Support Timeline

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 with useNuxtApp().payload. This global object is deleted after hydration in Nuxt 4.
  • pages:extend hook: switch to the new pages:resolved hook, which runs after page meta scanning.
  • dedupe boolean: replace refresh({ dedupe: true }) with refresh({ dedupe: 'cancel' }) and false with '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 of undefined. Use clearNuxtState('key', { reset: false }) for the old behavior.

A practical migration sequence for production applications:

  1. Run npx nuxt upgrade --dedupe and verify the application builds
  2. Run the codemod: npx codemod@latest nuxt/4/migration-recipe
  3. Move files to app/ (automated by the file-structure codemod)
  4. Update null checks to undefined in data fetching logic
  5. Test <KeepAlive> components with normalized names
  6. Update CI type checking to use vue-tsc -b --noEmit
  7. 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-recipe command automates directory restructuring, deprecated API replacements, and data fetching updates
  • Shallow reactivity via shallowRef improves 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

#nuxt
#vue
#migration
#typescript
#tutorial

Share

Related articles