Nuxt 4 Migration: Understanding the New Directory System

Nuxt 4 introduces a smarter, more scalable project structure that rethinks how apps are organized. Learn what’s changed, why it matters, and how to migrate from Nuxt 3 smoothly.

Charles Allotey
Nuxt 3

The Mastering Nuxt FullStack Unleashed Course is here!

Get notified when we release new tutorials, lessons, and other expert Nuxt content.

Click here to view course

Nuxt 4 doesn't just ship new features, it rethinks how your project is organized from the ground up. If you've been building with Nuxt 3, your first encounter with the new directory structure might raise a few eyebrows. But give it five minutes, and it clicks. The structure isn't just cleaner, it's smarter, and it sets a foundation that scales with you.

Let's break down exactly what changed, why it matters, and how to migrate your existing projects without losing your mind.

The Big Change: Everything Lives in app/ Now

In Nuxt 3, your application directories sat at the project root alongside config files, tooling setup, and everything else. It worked, but as projects grew, the root directory turned into a sprawling mess of concerns:

my-app/
├── components/
├── composables/
├── layouts/
├── middleware/
├── pages/
├── plugins/
├── utils/
├── app.vue
├── nuxt.config.ts
├── tailwind.config.ts
├── tsconfig.json
└── package.json

Nuxt 4 draws a clear line. Your application source code moves into an app/ directory, and everything else, from config files, the server layer, static assets all stays at the root:

my-app/
├── app/
│   ├── components/
│   ├── composables/
│   ├── layouts/
│   ├── middleware/
│   ├── pages/
│   ├── plugins/
│   ├── utils/
│   └── app.vue
├── server/
├── public/
├── nuxt.config.ts
└── package.json

The separation is deliberate. Open app/ and you're looking at everything that ships to the browser. Open server/ and you're in the Nitro layer. Everything outside of both is project infrastructure. No more hunting through a root directory full of config files to find your components folder.

Why This Structure was introduced

Performance - placing all your code in the root of your repo causes issues with .git/ and node_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 sure server/ 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.

There's also a practical win for monorepos. When your Nuxt app shares a repository with other services or packages, the app/ boundary makes it obvious where the frontend code ends.

What Stays at the Root

The move to app/ doesn't mean everything relocates. Several key directories stay exactly where they are, and for good reason:

server/ stays at the root. It's processed by Nitro, not by the Vue/Nuxt app layer, and keeping it separate makes the client/server split visually obvious. Your API routes, server middleware, and utilities under server/api/, server/middleware/, and server/utils/ are completely unchanged:

server/
├── api/
│   └── users.get.ts
├── middleware/
│   └── auth.ts
└── utils/
    └── db.ts

public/ stays at the root too. Static files served at the root URL path live here — public/favicon.ico becomes /favicon.ico — and that behavior is unchanged.

Config files (nuxt.config.ts, tsconfig.json, package.json) remain at the root, exactly where you expect them.

Migrating Without the Chaos: Compatibility Mode

Here's where Nuxt 4 shows real thoughtfulness. You don't have to restructure your entire project overnight. The compatibility mode lets you adopt the new structure incrementally perfect for production applications where big-bang refactors are a risk you don't want to take.

// nuxt.config.ts
export default defineNuxtConfig({
  future: {
    compatibilityVersion: 4,
  },
})

With this single config change, Nuxt resolves directories in order checking app/ first, then falling back to the project root. You can move directories one at a time, verify nothing breaks, and keep shipping throughout the migration.

This is the migration path I'd recommend for most teams. Move components/ one week, composables/ the next. The fallback mechanism means your project stays functional the entire time.

Moving Everything at Once

For smaller projects, a full migration in one pass is faster than a drawn-out incremental approach. Here's the exact sequence:

  1. Create an app/ directory at the root.
  2. Move components/, composables/, layouts/, middleware/, pages/, plugins/, and utils/ into it.
  3. Move app.vue into app/ as well.
  4. Leave server/, public/, and all config files at the root.
  5. Run nuxt dev and verify everything resolves correctly.

For the vast majority of projects, those five steps are the entire migration. Auto-imports and directory scanning are aware of the new structure, you don't need to update import paths inside your files unless you were using manual relative imports (which Nuxt discourages anyway).

Or can automate this migration by running

npx codemod@latest nuxt/4/file-structure

Auto-Imports: Still Magical, Zero Changes Required

One of the first things developers worry about with structural changes is whether auto-imports break. They don't. Nuxt's auto-import system scans the configured source directories regardless of where they live. Since app/ is now the source root, the scanner automatically picks up app/composables/, app/components/, and everything else.

The one area to watch is custom directories registered in nuxt.config.ts. If you've configured additional import paths, those need to be updated:

// Before (Nuxt 3)
export default defineNuxtConfig({
  imports: {
    dirs: ['composables/shared']
  }
})

// After (Nuxt 4)
export default defineNuxtConfig({
  imports: {
    dirs: ['app/composables/shared']
  }
})

Same applies to any custom component directories registered manually. Scan your nuxt.config.ts for any custom dirs arrays and update the paths. That's the full extent of manual config work for most projects.

TypeScript Path Aliases: One Gotcha to Know

Path aliases are where the migration can catch you off guard. In Nuxt 4 with the new directory structure, ~/ and @/ both point to the app/ directory. In Nuxt 3, they pointed to the project root.

For most imports this is a seamless transition, your ~/components/MyComponent.vue resolves correctly because components/ now lives inside app/. But if you have shared utilities, types, or helpers sitting outside app/ that are referenced with ~/, those paths will break.

The fix is to configure an alias that points back to the root:

export default defineNuxtConfig({
  alias: {
    '@root': '<rootDir>'
  }
})

Then update any imports that reference root-level files to use @root/ instead of ~/. This is the most common migration hiccup. Scan your codebase for ~/ imports that reference files outside of app/ before you finalize the migration.

Nuxt Layers: Migrate Together

If you're using Nuxt Layers to extend one project with another, both the layer and the consuming project should use the same directory convention. Mixing a Nuxt 3 flat-structure layer with a Nuxt 4 app/-based project creates unpredictable resolution behavior that's painful to debug.

The safest approach is to migrate layers alongside the main project. If that's not possible in your timeline, keep the layer on the Nuxt 3 flat structure and use compatibility mode in the consuming project until you can align both.

Summary

The Nuxt 4 directory change is one of those migrations that feels bigger than it is until you actually do it and then you wonder why the old structure ever made sense. Application code in app/, server code in server/, everything else at the root.

For teams with existing projects, compatibility mode makes this completely manageable. Set compatibilityVersion: 4 in your config, migrate directories at your own pace, and update the alias for any root-level imports that break.

For new projects, there's nothing to think about, the new structure is the default, and it's the right call from the start. The result is a project layout where every developer, on their first day, immediately understands what lives where.

Charles Allotey
Charles is a Frontend Developer at Vueschool. Has a passion for building great experiences and products using Vue.js and Nuxt.

Follow MasteringNuxt on