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.

Nuxt 4.0 Is Here: What’s New And What To Expect

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 in server/ 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

  1. Create an app/ directory in your project root
  2. Move the following folders into it (if they exist): assets, components, composables, layouts, middleware, pages, plugins, utils
  3. Move your root app.vue and error.vue files into app/

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 vs node) 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:

  1. Embrace skeleton loaders that mimic the final layout
  2. 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