Nuxt 4.0 is here: What’s new and what to expect
When a major new version of a framework is released, we often expect a list of flashy, headline-grabbing features. Nuxt 4, however, takes a different approach. This hype-free release focuses on long-term stability, performance, and a better developer experience rather than one-off features.
For developers currently on Nuxt 3, this means faster workflows, cleaner codebases, and more predictable applications. Whether you’re considering a Nuxt 4 migration, looking for improvements in TypeScript integration, or curious about the new data fetching behavior, this guide breaks down everything you need to know — and how to avoid common pitfalls when upgrading.
The new app/
directory
The first and most visible change you’ll notice in a fresh Nuxt 4 project is the new default app/
directory. All the familiar folders — components
, pages
, layouts
, etc. — now live inside this directory, creating a clear separation between your application code and your project’s root.
Performance and a cleaner workspace
This change might seem purely organizational, but it comes with two big technical benefits:
- Faster file watchers — By moving your application code into a dedicated directory, you isolate it from root-level files like
node_modules/
,.git/
,server/
, and config files. This gives Vite’s file watcher a smaller scope to monitor, resulting in faster Hot Module Replacement (HMR) and a more responsive development server. The improvement is especially noticeable on Windows and Linux, where file I/O is often a bottleneck - Improved IDE context — This structure helps your editor and other tools better understand your code. Files inside
app/
are clearly client-facing, while those inserver/
are server-side. The result is more accurate autocompletion, better type inference, and fewer confusing cross-environment errors
It’s important to note that this migration is optional. Nuxt 4 will detect the classic Nuxt 3 structure and continue to work as before if you choose not to migrate.
Migrating your project
Migrating an existing Nuxt 3 project is straightforward. Simply move all of Nuxt’s conventional application directories into the new app/
folder.
Nuxt 3 structure
my-nuxt-app/ ├─ components/ ├─ layouts/ ├─ pages/ ├─ plugins/ ├─ public/ ├─ server/ ├─ app.vue └─ nuxt.config.ts
Nuxt 4 structure
my-nuxt-app/ ├─ app/ │ ├─ components/ │ ├─ layouts/ │ ├─ pages/ │ ├─ plugins/ │ └─ app.vue ├─ public/ ├─ server/ └─ nuxt.config.ts
Migration steps
- Create an
app/
directory in your project root - Move the following folders into it (if they exist):
assets
,components
,composables
,layouts
,middleware
,pages
,plugins
,utils
- Move your root
app.vue
anderror.vue
files intoapp/
That’s it — Nuxt will automatically detect and use the new structure.
Troubleshooting migration issues
While moving files is simple, a few knock-on effects may require adjustments:
Custom scripts and CI/CD pipelines
Any hardcoded paths in `package.json` or CI/CD configs (e.g., GitHub Actions, GitLab CI) will need updating.
For example:
// OLD "scripts": { "lint": "eslint ./pages" }
This needs to be updated to point to the new location:
// NEW "scripts": { "lint": "eslint ./app/pages" }
Configuration files
Files like .nuxtignore
may also need updates. Before:
components/LegacyComponent.vue
After:
app/components/LegacyComponent.vue
Be sure to check testing setups (Vitest, Jest) and Storybook configs for outdated paths.
Navigating the new TypeScript experience
Less visible than the app/
directory but just as important, Nuxt 4 introduces a revamped TypeScript setup that eliminates longstanding issues with type safety and context confusion.
Eliminating ‘type-bleed’
In Nuxt 3, a single monolithic tsconfig.json
handled types across the entire project. This often caused “type-bleed,” where types from one environment (like server-side Node.js utilities) leaked into another (like client-side Vue components). The result was hidden bugs or confusing runtime errors.
Nuxt 4 fixes this by creating separate, virtual TypeScript projects for each context: app/
, server/
, and shared/
. This strict separation ensures precise type inference. Server-only utilities are correctly flagged as unavailable in client-side code — and vice versa.
Simplifying tsconfig.json
For most projects, upgrading means removing complexity. Nuxt now handles path mapping and context separation internally.
Nuxt 3 example
{ "extends": "./.nuxt/tsconfig.json", "compilerOptions": { "strict": true, "baseUrl": ".", "paths": { "~/*": ["./*"], "@/*": ["./*"], "~~/*": ["./*"], "@@/*": ["./*"] } }, "include": [ "./components/**/*", "./pages/**/*" ] }
Nuxt 4 example
{ "compilerOptions": { "strict": true // Add any overrides you need } }
By letting Nuxt handle the boilerplate, your config is cleaner and less error-prone.
Surfacing hidden type errors
The stricter typing may reveal errors that went unnoticed in Nuxt 3. While this may feel like extra work during migration, it greatly improves long-term stability.
For example, accidentally importing a server utility into a client component:
// server/utils/log-to-file.ts import fs from 'node:fs' export function logToFile(message: string) { fs.appendFileSync('server.log', message + 'n') }
<script setup lang="ts"> import { logToFile } from '~/server/utils/log-to-file' function handleClick() { logToFile('Button was clicked') //Browser crash in Nuxt 3 } </script>
With Nuxt 3, this might slip through type checks and only fail at runtime. With Nuxt 4, the new system flags it immediately with an error like this:
Cannot find module 'node:fs' or its corresponding type declarations.
Guidance for module authors
If you’re building Nuxt modules, this change requires more deliberate structuring:
- Separate concerns — Provide clear entry points for client vs. server code
- Check
package.json
exports — Use conditional exports (browser
vsnode
) to ensure Nuxt resolves types correctly
Data fetching refinements
Nuxt 4 also changes the default behavior of useAsyncData
and useFetch
. The API is the same, but the logic now prioritizes freshness over staleness.
The new default
In Nuxt 3, stale data was preserved while new data loaded in the background. This prevented loading flashes but meant users briefly saw outdated content.
Nuxt 4 clears old data immediately on refetch. The data
ref is set to null
while pending
is true
, making it explicit that old data is no longer valid.
<div v-if="!pending && data"> <h1>{{ data.title }}</h1> <p>{{ data.body }}</p> </div> <div v-else> Loading... </div>
This encourages developers to design proper loading states instead of relying on stale content.
Restoring the old behavior
If you prefer the “stale-while-revalidate” pattern, you can implement it manually:
const displayData = ref(data.value) watch(data, (newData) => { if (newData) { displayData.value = newData } })
This ensures users always see existing content until new data arrives.
Avoiding UI flashes
The biggest risk is UI flashes, where large content blocks disappear and are replaced by spinners. Best practices:
- Embrace skeleton loaders that mimic the final layout
- Manually preserve stale data if a seamless UX is critical
Conclusion
In a landscape obsessed with flashy new features, Nuxt 4 takes a more mature path. This release focuses on refinement:
- A cleaner, faster workspace via the
app/
directory. - A stricter TypeScript setup that prevents bugs before they ship.
- Smarter defaults for data fetching that emphasize reliability and clarity.
Nuxt 4 isn’t about a headline-grabbing feature. Its real value lies in the sum of these improvements — an investment in smoother, faster, and more stable application development at scale.
The post Nuxt 4.0 is here: What’s new and what to expect appeared first on LogRocket Blog.
This post first appeared on Read More