Nuxt 4 Migration Guide: app/ Directory Restructuring and Gradual Adoption
Hello, this is Lovefield.
Since the release of Nuxt 3, countless projects have flourished on the framework. However, as codebases grew, a common point of friction emerged: project layout. Having directories like pages/, components/, and composables/ sit directly at the root frequently led to UI code mingling with Docker configs, CI/CD scripts, monorepo settings, and Nitro backend server code (server/).
In Nuxt 4, this architectural limitation is solved by establishing the app/ directory as the standard application root. In this post, we will examine the key architectural shifts in Nuxt 4 and outline a step-by-step strategy for a seamless, breakage-free migration.
1. Key Changes: What Has Evolved?
1-1. Consolidating Frontend Code into app/
The most noticeable layout change in Nuxt 4 is the relocation of all frontend-related application code inside the app/ directory.
Plain Text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Nuxt 3 Classic Layout
my-nuxt-app/
├── components/
├── composables/
├── layouts/
├── pages/
├── server/
├── app.vue
├── nuxt.config.ts
├── package.json
└── Dockerfile
# Nuxt 4 Recommended Layout
my-nuxt-app/
├── app/ # 👈 Dedicated frontend application layer
│ ├── components/
│ ├── composables/
│ ├── layouts/
│ ├── pages/
│ ├── app.vue
│ ├── router.options.ts
│ └── error.vue
├── server/ # 👈 Dedicated Nitro server engine & API layer
│ ├── api/
│ └── middleware/
├── public/
├── nuxt.config.ts
├── package.json
└── DockerfileThis restructuring brings tangible benefits in production environments:
Clear Isolation between Infra and UI: Configuration files (Dockerfile, CI pipelines, linters) no longer clutter the visual view layer.
Distinct Full-Stack Boundaries: The server/ directory functions purely as an isolated Nitro backend runtime, while app/ strictly handles SSR/client rendering—preventing accidental code leakage and circular dependencies.
2. Gradual Migration via compatibilityVersion
Moving all directories at once in an ongoing project can trigger cascade issues with Git history, relative path resolutions, and auto-imports. The Nuxt core team addressed this by introducing the future.compatibilityVersion flag.
2-1. Configuring nuxt.config.ts
Before moving any files, keep your current Nuxt 3.x installation and enable compatibilityVersion: 4:
TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
// nuxt.config.ts
export default defineNuxtConfig({
// Enable Nuxt 4 compatibility mode
future: {
compatibilityVersion: 4,
},
typescript: {
strict: true,
typeCheck: true,
},
});Enabling this option instructs the build pipeline to emulate Nuxt 4's behavior. If no app/ directory exists yet, Nuxt automatically falls back to the existing Nuxt 3 root structure, ensuring your build will not break abruptly.
3. Step-by-Step Migration Process
Step 1: Move Frontend Directories
Move all frontend source files into the new app/ folder, leaving server/, public/, node_modules/, and root configuration files intact.
Shell
1
2
mkdir app
git mv components composables layouts pages middleware plugins utils app.vue error.vue app/> Tip: Use git mv instead of standard file dragging to preserve complete Git commit histories.
Step 2: Resolve Path Aliases
Nuxt 4 formalizes alias resolutions as follows:
~ or @: Points to the app/ directory by default.
~~ or @@: Points to the project root directory.
If your code previously used ~/ to reference root files (e.g., package.json), update them to ~~/.
TypeScript
1
2
3
4
5
6
7
8
// ❌ Referencing a root file with ~/ (In Nuxt 4, this looks under app/package.json)
import pkg from '~/package.json';
// ✅ Use @@ or ~~~ to reference project root
import pkg from '~~/package.json';
// ✅ Use @ or ~ to reference internal app/ components and modules
import BaseButton from '@/components/common/BaseButton.vue';Step 3: Refresh Types via nuxi prepare
After reorganizing folders, regenerate Nuxt's auto-generated types so that your IDE and TypeScript compiler recognize the new layout:
Shell
1
npx nuxi prepare4. Nitro Server Separation & Optimization
In Nuxt 4, the server/ directory benefits from improved type isolation and distinct compilation targets.
Sharing Types Across Boundaries
When sharing Data Transfer Objects (DTOs) between frontend (app/) and backend (server/), a frequent pitfall in Nuxt 3 was importing arbitrary files from types/.
Recommended pattern for Nuxt 4:
Pure TypeScript type / interface: Place shared contracts under a root directory like shared/types/ and import them strictly via import type.
Runtime Validators (Zod, Valibot, etc.): Ensure runtime validation schemas do not inflate client bundle sizes unnecessarily.
TypeScript
1
2
3
4
5
6
// shared/types/user.ts
export interface UserDTO {
id: string;
nickname: string;
role: 'admin' | 'member';
}TypeScript
1
2
3
4
5
6
7
8
9
10
// server/api/user.get.ts
import type { UserDTO } from '~~/shared/types/user';
export default defineEventHandler((): UserDTO => {
return {
id: 'u-1001',
nickname: 'Lovefield',
role: 'admin',
};
});5. Migration Checklist
Set future.compatibilityVersion: 4 in nuxt.config.ts: Safely preview Nuxt 4 features before directory moves.
Move files via git mv: Isolate the view layer into app/ without breaking Git blame history.
Audit Path Aliases: Reserve ~~/ for root references and ~/ for app/ references.
Run nuxi prepare: Regenerate .nuxt/tsconfig.json and auto-import signatures.
Optimize Docker Caching: Take advantage of the split structure to cache COPY app ./app and COPY server ./server layers independently.
Nuxt 4's layout redesign is more than a superficial directory shift—it provides clean separation between platform infrastructure and client logic in complex applications. If you maintain production Nuxt applications, leveraging the future flag offers a smooth, risk-free path forward.
This article was generated by AI.

Lovefield
Web Front-End developer
하고싶은게 많고, 나만의 서비스를 만들고 싶은 변태스러운 개발자입니다.

컨테이너 취약점 제로를 향해: Chainguard(cgr.dev)와 Docker Hardened Images(dhi.io) 비교
Docker