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.
Written by Francisco Zapata
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: 4in 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