Back
Nuxt

Complete Guide: Migrating to Nuxt 4 from Nuxt 3

Everything you need to know to migrate your project from Nuxt 3 to Nuxt 4. Key changes, new directory structure, and detailed steps.

Francisco ZapataWritten by Francisco Zapata
December 22, 202513 min read
Complete Guide: Migrating to Nuxt 4 from Nuxt 3

Nuxt 4 has arrived with significant improvements in performance, developer experience, and conventions. If you have a Nuxt 3 project, this guide will walk you through the migration process step by step, covering every important change and how to adapt without breaking your application.

What Is New in Nuxt 4?

Nuxt 4 is not a complete rewrite like Nuxt 3 was compared to Nuxt 2. It is more of a natural evolution that consolidates best practices and adopts the defaults the community has been asking for. The main changes include:

  • New directory structure with an app/ folder
  • Performance improvements in build and runtime
  • Better compatibility with the Vue ecosystem
  • New conventions that simplify development
  • TypeScript improvements with more precise types

Preparation: Enable Compatibility Flags

Before jumping straight to Nuxt 4, you can prepare your project by enabling compatibility flags in Nuxt 3. This lets you adopt changes gradually:

// nuxt.config.ts (in your Nuxt 3 project)
export default defineNuxtConfig({
  future: {
    compatibilityVersion: 4
  }
})

With this configuration, Nuxt 3 will start behaving like Nuxt 4 in areas where behavior changes exist. This allows you to detect and fix issues before the actual migration.

Main Change: The New Directory Structure

The most visible change in Nuxt 4 is the reorganization of the directory structure. Most application code moves inside an app/ folder:

Nuxt 3 Structure:

project/
├── components/
├── composables/
├── layouts/
├── middleware/
├── pages/
├── plugins/
├── app.vue
├── error.vue
├── nuxt.config.ts
└── package.json

Nuxt 4 Structure:

project/
├── app/
│   ├── components/
│   ├── composables/
│   ├── layouts/
│   ├── middleware/
│   ├── pages/
│   ├── plugins/
│   ├── app.vue
│   └── error.vue
├── public/
├── server/
├── shared/
│   ├── types/
│   └── utils/
├── nuxt.config.ts
└── package.json

Why This Change?

The separation into app/, server/, and shared/ has clear benefits:

1. Separation of concerns: Client, server, and shared code are clearly delimited.

2. Better organization: In large projects, having everything at the root becomes chaotic.

3. Explicit shared code: The shared/ folder contains types and utilities used in both client and server.

Step by Step: Migration

Step 1: Update Dependencies

# Update nuxt to version 4
npx nuxi upgrade --dedupe

# Or install directly
npm install nuxt@latest

Verify that your modules are compatible with Nuxt 4:

npx nuxi module search <module-name>

Step 2: Move Files

Create the app/ folder and move the corresponding directories:

mkdir -p app

# Move main directories
mv components app/
mv composables app/
mv layouts app/
mv middleware app/
mv pages app/
mv plugins app/
mv app.vue app/
mv error.vue app/

# Create shared if you need shared code
mkdir -p shared/types shared/utils

Step 3: Update nuxt.config.ts

Nuxt 4 automatically detects the app/ structure, but you can customize it:

// nuxt.config.ts
export default defineNuxtConfig({
  dir: {
    app: 'app',
    shared: 'shared'
  }
})

Step 4: Update Imports

Auto-imports continue working the same way. However, if you had manual imports with relative paths, update them:

// Before (Nuxt 3)
import { useMyComposable } from '~/composables/useMyComposable'

// After (Nuxt 4) - the ~ alias points to app/
import { useMyComposable } from '~/composables/useMyComposable'
// Or use the #app alias
import { useMyComposable } from '#app/composables/useMyComposable'

The ~ alias in Nuxt 4 points to the app/ folder instead of the project root.

Step 5: Set Up the shared/ Folder

If you have types or utilities shared between client and server, move them to shared/:

// shared/types/user.ts
export interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'user'
}

// shared/utils/format.ts
export function formatDate(date: Date): string {
  return new Intl.DateTimeFormat('en-US').format(date)
}

These files are auto-imported in both client and server code.

Configuration Changes

TypeScript Compatibility

Nuxt 4 significantly improves TypeScript support:

// nuxt.config.ts
export default defineNuxtConfig({
  typescript: {
    strict: true,
    typeCheck: true
  }
})

Route Rules Changes

The routeRules syntax remains the same, but there are new options:

export default defineNuxtConfig({
  routeRules: {
    '/api/**': { cors: true },
    '/blog/**': { swr: 3600 },
    '/admin/**': { ssr: false },
    '/old-page': { redirect: '/new-page' }
  }
})

Changes in the Server Directory

The server/ folder remains outside app/ with the same structure:

server/
├── api/
│   └── users.get.ts
├── middleware/
│   └── auth.ts
├── plugins/
│   └── database.ts
├── routes/
│   └── health.get.ts
└── utils/
    └── db.ts

Files in server/utils/ and types in shared/types/ are auto-imported.

Handling Modules and Plugins

Community Modules

Most popular modules are already compatible with Nuxt 4. Check versions:

{
  "dependencies": {
    "@nuxtjs/i18n": "^9.0.0",
    "@nuxt/ui": "^3.0.0",
    "@nuxt/image": "^1.8.0",
    "@nuxt/icon": "^1.6.0",
    "@pinia/nuxt": "^0.8.0"
  }
}

Plugins

Plugins move to app/plugins/ and keep the same syntax:

// app/plugins/analytics.client.ts
export default defineNuxtPlugin((nuxtApp) => {
  if (import.meta.client) {
    console.log('Analytics initialized')
  }
})

Data Fetching Changes

useAsyncData and useFetch

The API remains the same, with improvements in cache handling:

const { data, pending, error, refresh } = await useFetch('/api/users')

// New option: getCachedData for cache customization
const { data } = await useFetch('/api/users', {
  getCachedData(key, nuxtApp) {
    return nuxtApp.payload.data[key] || nuxtApp.static.data[key]
  }
})

Change in dedupe Default Value

In Nuxt 4, dedupe changes its default to 'cancel' instead of 'defer'. This means that if a new request is made while another is in progress, the previous one is cancelled:

// To keep Nuxt 3 behavior:
const { data } = await useFetch('/api/users', {
  dedupe: 'defer'
})

Common Issues and Solutions

1. Import errors after moving files

If you see module resolution errors, clear the cache:

npx nuxi cleanup
rm -rf .nuxt node_modules/.cache
npm run dev

2. Incompatible modules

If a module does not support Nuxt 4 yet, you can use the compatibility layer:

export default defineNuxtConfig({
  modules: [
    ['legacy-module', { /* options */ }]
  ]
})

3. TypeScript errors with paths

Update your tsconfig.json if you have custom paths:

{
  "extends": "./.nuxt/tsconfig.json",
  "compilerOptions": {
    "paths": {
      "#shared/*": ["./shared/*"]
    }
  }
}

Migration Checklist

  • [ ] Enable compatibilityVersion: 4 in Nuxt 3 and fix errors
  • [ ] Update Nuxt to version 4
  • [ ] Create app/ folder and move files
  • [ ] Create shared/ folder for shared code
  • [ ] Update manual imports
  • [ ] Verify module compatibility
  • [ ] Run tests
  • [ ] Clear cache and rebuild
  • [ ] Verify in production

Conclusion

Migrating to Nuxt 4 from Nuxt 3 is much smoother than previous migrations. The gradual approach with compatibility flags allows you to transition at your own pace. The main changes are organizational (new directory structure) rather than API changes, which greatly simplifies the process. Take advantage of the new conventions to improve your project structure and enjoy the performance improvements this version brings.

Comments (0)

Leave a comment

Be the first to comment