frontend·10.06.2026·8 min read

Migrating to Nuxt 4 - The New app/ Directory, Smarter Data Fetching, and a Painless Upgrade

Nuxt 4 is here. Learn the new app/ directory structure, improved useFetch/useAsyncData, the path-alias gotcha, and how to upgrade safely with codemods.

Migrating to Nuxt 4 - new app directory and data fetching

I'll be honest — I put off this migration for longer than I should have. "It'll probably break something" is the lie I kept telling myself. When I finally sat down and did it, the whole thing took a morning. Nuxt 4 shipped in July 2025 with a clear mandate: make the upgrade painless. Most breaking changes were previewed behind a compatibility flag for over a year, every sharp edge has a config escape hatch, and a codemod handles the mechanical parts. This guide covers everything that actually bit me — and what didn't.

What Changed (The Short Version)

  • app/ directory — application code moves into an isolated subtree; Vite's watcher scope shrinks
  • Smarter useAsyncData / useFetch — shared keys deduplicate requests, reactive keys auto-refetch, data cleans up on unmount
  • TypeScript multi-project — separate TS projects for app / server / shared / builder code; one root tsconfig.json is all you need
  • Compatibility-first — the classic Nuxt 3 layout still works; nothing breaks on a bare version bump

The New app/ Directory — Why I Actually Like It

In Nuxt 4, your application code lives inside app/. The root is reserved for configuration, server code, and content. No more sifting through node_modules entries and server/ routes just to find a component.

project/
├── app/                   ← all application code lives here
│   ├── app.vue
│   ├── app.config.ts
│   ├── error.vue
│   ├── components/
│   ├── composables/
│   ├── layouts/
│   ├── middleware/
│   ├── pages/
│   ├── plugins/
│   └── utils/
├── content/
├── public/
├── server/
├── shared/
├── nuxt.config.ts
└── tsconfig.json

This is exactly the layout this blog runs on — and the headless Nuxt + Shopware stack I use for storefront work follows the same pattern — so I can tell you from experience it holds up in production.

Why it's faster: Vite watches a smaller directory tree. With node_modules, .git, and server/ out of scope, HMR is noticeably snappier on large projects. If you care about development-loop performance the same way you care about Core Web Vitals in production, this is worth the migration.

Moving your files:

mkdir -p app
mv components composables layouts middleware pages plugins utils app/
mv app.vue app.config.ts error.vue app/ 2>/dev/null || true

This migration is optional. Nuxt 4 detects the classic Nuxt 3 layout and keeps working. You choose when — or whether — to move.

The ~/ Alias Gotcha — This Is the One That Got Me

Read this before you do anything else. I wish someone had told me.

Before Nuxt 4: ~/ and @/ resolved to the project root.
After Nuxt 4 with app/: both resolve to app/.

Any file outside app/ — shared types, root-level helpers, config constants — that you import via ~/ will throw a "module not found" error at build time. I had a handful of shared type files sitting at the project root, all imported with ~/types/..., and they all blew up the moment I moved to the new structure. Easy to fix once you know, infuriating to diagnose if you don't.

Fix option 1 — add a root alias in nuxt.config.ts:

import { fileURLToPath } from 'node:url'

export default defineNuxtConfig({
  alias: {
    '~root': fileURLToPath(new URL('./', import.meta.url)),
  },
})

Then update affected imports: ~/types/product → ~root/types/product.

Fix option 2 — move shared code into app/ or shared/:

Anything used only by the app belongs in app/. Anything shared between app and server belongs in shared/ — Nuxt auto-imports from there and the alias change doesn't affect it.

Run a quick search before migrating:

# find imports that cross the app/ boundary
grep -r 'from ~/' app/ | grep -v 'from ~/app/'

Data Fetching Upgrades — Three Things Worth Knowing

useAsyncData and useFetch got three meaningful improvements. In my experience, the shared-key deduplication alone pays back the migration cost on any app with nested layouts.

Shared Keys — no more duplicate requests

Components that call useAsyncData with the same key now share the result. Previously, two components mounted on the same page each triggered their own request even for identical data. I've seen this cause noticeable flicker in product listing pages — now it just works.

Reactive Keys — automatic refetch

Pass a function (or computed) as the key and the composable refetches whenever it changes:

<script setup lang="ts">
const route = useRoute()

const { data: product } = await useAsyncData(
  // reactive key — changes when the route param changes
  () => `product-${route.params.slug}`,
  () => $fetch(`/api/products/${route.params.slug}`)
)
</script>

<template>
  <div v-if="product">
    <h1>{{ product.name }}</h1>
  </div>
</template>

When the user navigates from /products/shirt to /products/jacket, the key changes, the old data is cleaned up, and a fresh fetch fires — without any manual watch. My rule of thumb used to be "add a watcher and call refresh()"; now I just reach for a reactive key.

Automatic cleanup

Data registered via useAsyncData is now cleared when the owning component unmounts. In long-lived SPAs this was a subtle memory leak; Nuxt 4 handles it transparently. I didn't realise how often I was leaking state until I wasn't anymore.

Quick mental model:

  • $fetch — raw request, runs every time, no caching
  • useFetch — smart wrapper; fetches once on server, rehydrates on client
  • useAsyncData — full control; reactive key, manual refresh(), custom transforms

Shopware Frontends leans heavily on useFetch + reactive keys for its catalog pages — the Nuxt 4 upgrade makes that pattern cheaper out of the box.

TypeScript — One Config, Four Projects

Nuxt 4 sets up separate TypeScript projects for each environment: app, server, shared, and the Nuxt builder itself. This gives you:

  • Accurate autocompletion — server-only globals like H3Event don't bleed into component files
  • Correct inference — no more false "cannot find name useNuxtApp" in server routes
  • Faster tsc — each project is smaller and checked in isolation

You only maintain one tsconfig.json at the project root. Nuxt generates the sub-configs in .nuxt/ during nuxi prepare. Nothing to hand-edit. I was slightly worried this would make IDE setup more complex — it didn't. If anything, autocomplete in VS Code got more accurate.

Removed APIs — Check These Before Upgrading

A few things disappeared in Nuxt 4:

RemovedWhat to do
generate.excludeUse routeRules in nuxt.config.ts to control pre-rendering per route
generate.routesSame — routeRules or a nitro.prerender.routes array
Nuxt 2 compatibility in @nuxt/kitModule authors: drop legacy defineNuxtModule patterns that targeted Nuxt 2

If you maintain a custom Nuxt module, run npx nuxi module check to surface compatibility issues before upgrading the consuming app. I hit the @nuxt/kit one on a client project with a homegrown module — nuxi module check flagged it immediately.

How I Actually Did the Upgrade — 5 Steps

  1. Bump the package — bun add nuxt@latest (or npm/pnpm equivalent)
  2. Run the codemod — automates the majority of mechanical changes:
    npx codemod@latest nuxt/4/migration-recipe
    
  3. Move app/ code — run the mkdir/mv commands from the section above, or skip and stay on the Nuxt 3 layout
  4. Fix alias imports — audit ~/ references outside app/; apply the ~root alias or move files to shared/
  5. Check removed APIs — search for generate.exclude, generate.routes, and any Nuxt 2 module patterns; replace per the table above

Honestly, most projects are done after step 2. The app/ move and alias fixes are only relevant if you opt into the new directory structure — and given that the alias thing is what got me, I'd say audit your imports first regardless.

Support window: Nuxt 3 receives support for at least 6 months after Nuxt 4's release date (July 15, 2025). Plan your migration before January 2026; anything after that is on borrowed time.

Worth Doing

Nuxt 4 is a well-engineered major version bump — the compatibility flag approach meant most teams hit zero surprises on release day, and that matched my experience. The app/ directory structure is the biggest conceptual shift, but it's genuinely useful: faster HMR, cleaner project root, and an obvious home for application code. The data fetching improvements pay dividends on any project with nested routes or shared components that hit the same endpoints.

If you're building or migrating a headless Shopware storefront with Nuxt, this is worth doing now rather than carrying the Nuxt 3 layout indefinitely. I help teams navigate exactly these migrations — feel free to reach out if you want a second pair of eyes on your upgrade.

Useful References

Enjoyed this?

Get new posts as they land.

Subscribe via RSS

Keep reading