---
title: "Nuxt 4.0 is here: the app/ directory, data fetching changes and the upgrade · vuejs.school"
description: "Nuxt 4.0.0 was published on 15 July. A new app/ folder, saner data fetching defaults, split TypeScript projects and a faster CLI, with few surprises."
canonical_url: "https://vuejs.school/community/blog/nuxt-4-is-here"
meta:
  "og:description": "Nuxt 4.0.0 was published on 15 July. A new app/ folder, saner data fetching defaults, split TypeScript projects and a faster CLI, with few surprises."
  "og:image": "https://vuejs.school/_og/d/a_~TnV4dCA0LjAgaXMgaGVyZTogdGhlIGFwcC8gZGlyZWN0b3J5LCBkYXRhIGZldGNoaW5nIGNoYW5nZXMgYW5kIHRoZSB1cGdyYWRlIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~TnV4dCA0LjAgaXMgaGVyZTogdGhlIGFwcC8gZGlyZWN0b3J5LCBkYXRhIGZldGNoaW5nIGNoYW5nZXMgYW5kIHRoZSB1cGdyYWRl%2Cdescription_~TnV4dCA0LjAuMCB3YXMgcHVibGlzaGVkIG9uIDE1IEp1bHkuIEEgbmV3IGFwcC8gZm9sZGVyLCBzYW5lciBkYXRhIGZldGNoaW5nIGRlZmF1bHRzLCBzcGxpdCBUeXBlU2NyaXB0IHByb2plY3RzIGFuZCBhIGZhc3RlciBDTEksIHdpdGggZmV3IHN1cnByaXNlcy4%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9udXh0LTQtaXMtaGVyZSI%2Cs_HlBA7Tz2I1Mf9L9N.png"
  "og:image:alt": "Nuxt 4.0 is here: the app/ directory, data fetching changes and the upgrade · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Nuxt 4.0 is here: the app/ directory, data fetching changes and the upgrade · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~TnV4dCA0LjAgaXMgaGVyZTogdGhlIGFwcC8gZGlyZWN0b3J5LCBkYXRhIGZldGNoaW5nIGNoYW5nZXMgYW5kIHRoZSB1cGdyYWRlIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~TnV4dCA0LjAgaXMgaGVyZTogdGhlIGFwcC8gZGlyZWN0b3J5LCBkYXRhIGZldGNoaW5nIGNoYW5nZXMgYW5kIHRoZSB1cGdyYWRl%2Cdescription_~TnV4dCA0LjAuMCB3YXMgcHVibGlzaGVkIG9uIDE1IEp1bHkuIEEgbmV3IGFwcC8gZm9sZGVyLCBzYW5lciBkYXRhIGZldGNoaW5nIGRlZmF1bHRzLCBzcGxpdCBUeXBlU2NyaXB0IHByb2plY3RzIGFuZCBhIGZhc3RlciBDTEksIHdpdGggZmV3IHN1cnByaXNlcy4%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9udXh0LTQtaXMtaGVyZSI%2Cs_HlBA7Tz2I1Mf9L9N.png"
  "twitter:image:alt": "Nuxt 4.0 is here: the app/ directory, data fetching changes and the upgrade · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## What shipped

The [announcement](https://nuxt.com/blog/v4) groups the release under four headings. With the details from the [upgrade guide](https://nuxt.com/docs/4.x/getting-started/upgrade), they are:

- A new project structure: application code moves into `app/`, with `server/`, `shared/`, `public/`, `layers/` and `modules/` staying at the root.
- Smarter data fetching: components that call `useAsyncData` or `useFetch` with the same key share one set of refs, data is cleaned up when the last component using it unmounts, keys can be reactive, and `data` is shallow by default.
- A better TypeScript setup: Nuxt generates separate TypeScript projects for app, server, `shared/` and build-time code, so the editor stops suggesting browser APIs in server code, and server APIs in app code.
- A faster CLI: the CLI and the Vite dev server talk over an internal socket instead of a network port, file watching uses native `fs.watch`, and the Node.js compile cache is reused between starts.

## The app/ directory: what moves and why

In Nuxt 4, a project with the usual folders looks like this:

```
my-app/
  app/
    app.vue
    assets/
    components/
    composables/
    layouts/
    middleware/
    pages/
    plugins/
    utils/
  server/
    api/
  shared/
  public/
  nuxt.config.ts
  tsconfig.json
  package.json
```

The announcement gives two reasons for the move:

- **Faster file watching.** With code at the project root, file watchers also scanned `node_modules/` and `.git/`. Moving app code one level down lets Nuxt watch a smaller tree, which the announcement says matters most on Windows and Linux.
- **Clearer contexts.** The split tells the editor which files are client code and which are server code, which is what makes the TypeScript separation below possible.

`~` and `@` now point at `app/`, so `~/components/Button.vue` still resolves once the folder has moved.

Nothing forces the move today. Nuxt detects a project that still has `pages/` at the root and keeps using it. If you want to upgrade the framework now and move the folders later, pin the old layout explicitly so the detection cannot surprise you:

*nuxt.config.ts*

```
export default defineNuxtConfig({
  srcDir: '.',
  dir: {
    app: 'app',
  },
})
```

`srcDir: '.'` keeps your application code at the root. `dir.app` is the folder where Nuxt looks for `router.options.ts` and `spa-loading-template.html`, so they stay in `app/` as they were in Nuxt 3.

## Data fetching: the defaults that changed

Here is a page written for the 4.0 defaults. It loads one post by the `id` in its URL:

*app/pages/posts/\[id].vue*

```
<script setup lang="ts">
type Post = { id: number; title: string; body: string }

const route = useRoute()
const id = computed(() => Number(route.params.id))

const { data: post, status, error, refresh } = await useFetch<Post>(() => `/api/posts/${id.value}`)
</script>

<template>
  <p v-if="status === 'pending'">Loading…</p>
  <p v-else-if="error">Could not load this post.</p>
  <article v-else-if="post">
    <h1>{{ post.title }}</h1>
    <p>{{ post.body }}</p>
    <button type="button" @click="refresh()">Reload</button>
  </article>
</template>
```

Four of the changes show up in this page:

- **`data` is shallow.** `post` is a `shallowRef`. Replacing `post.value` updates the page; writing `post.value.title = 'Draft'` does not. If a page edits fetched data in place, pass `{ deep: true }` to that one call rather than switching the project-wide default back, which the guide advises against.
- **Empty means `undefined`.** Before the first response, `post` is `undefined`, not `null`. So `v-else-if="post"` is the right guard, and an `=== null` check no longer catches the empty state.
- **`pending` follows `status`.** `pending` is now `true` only while `status` is `'pending'`, which is why the template reads `status` directly. With `immediate: false`, `pending` stays `false` until the first request instead of starting `true`.
- **A reactive URL refetches.** The URL is a function of `id`, so moving from one post to another fetches the new one. The guide notes one edge: with `immediate: false`, a key change refetches only after a first fetch has happened, so trigger the first one yourself.

Calls that share a key now share state. Two components calling `useFetch` with the same key get the same `data`, `error` and `status` refs, so they must agree on options: in development, Nuxt warns when two calls with one key use a different `transform`, `pick`, `default` or `deep`. And `refresh({ dedupe: true })` is gone: pass `'cancel'` to replace the running request or `'defer'` to keep it.

## TypeScript: separate projects and a stricter check

Nuxt now generates `tsconfig.app.json`, `tsconfig.server.json`, `tsconfig.shared.json` and `tsconfig.node.json` (for build-time code such as `nuxt.config.ts`) inside `.nuxt/`. A new project's root `tsconfig.json` only references them:

*tsconfig.json*

```
{
  "files": [],
  "references": [
    { "path": "./.nuxt/tsconfig.app.json" },
    { "path": "./.nuxt/tsconfig.server.json" },
    { "path": "./.nuxt/tsconfig.shared.json" },
    { "path": "./.nuxt/tsconfig.node.json" }
  ]
}
```

An upgraded project does not have to switch: its single `extends` line keeps working. To opt in, replace the file with the one above and run `vue-tsc -b --noEmit` instead of `vue-tsc --noEmit`. After the switch, type augmentations (`declare module` files) need to sit in `app/`, `server/` or `shared/` to reach the right project; one in a root `types/` folder is no longer picked up.

The announcement expects the TypeScript change to cause more surprises than any other. One of them reaches every project, switched or not: `noUncheckedIndexedAccess` is now on, so TypeScript treats `items[0]` as possibly `undefined`, and `items[0].name` becomes an error until you handle that case. Fix those where you can. If the backlog is too long for one afternoon, `typescript.tsConfig.compilerOptions.noUncheckedIndexedAccess: false` in `nuxt.config.ts` turns the rule back off.

## Other breaking changes and their opt-outs

The upgrade guide rates each change by impact and gives an opt-out for most. Beyond the folders, the data layer and TypeScript, these are the ones a typical app can meet:

- Component names are normalised to match auto-import names, so a component in `components/forms/Input.vue` is `FormsInput` in `findComponent` and in `<KeepAlive include>`. Opt out with `experimental.normalizeComponentNames: false`.
- Unhead v2: the `vmid`, `hid`, `children` and `body` meta props are removed. Opt out with `unhead.legacy: true` while you clean up.
- `route.meta.name` and `route.meta.path` are no longer duplicated from `definePageMeta`; read `route.name` and `route.path`.
- The SPA loading template renders next to `#__nuxt` rather than inside it, which can move a CSS selector.
- `error.data` is parsed for you in `error.vue`; a `JSON.parse(error.data)` now throws.
- Only component CSS is inlined in the HTML; global CSS is linked. `features.inlineStyles: true` restores the old behaviour.
- `generate.routes` and `generate.exclude` move to `nitro.prerender.routes` and `nitro.prerender.ignore`.
- `window.__NUXT__` is removed after hydration; read `useNuxtApp().payload`.
- Modules from layers now load before the project's own, so the project's modules win.
- Setting `compatibilityVersion: 3` no longer has any effect.

Module authors have a few more: `builder:watch` emits absolute paths and the `lodash/template` based template helpers are gone (both with codemods), and `@nuxt/kit` drops Nuxt 2 support.

## Upgrade now or wait for your modules

Upgrade this week if every module in your `nuxt.config.ts` has a release that declares Nuxt 4 support in its changelog or README. Wait if a module you depend on has not: the announcement warns that a few modules may need updates for full Nuxt 4 compatibility, and no codemod can update a module for you. Nuxt 3's maintenance window makes waiting cheap.

The announcement also says what the next major brings: new major versions of Nitro and h3, and the Vite Environment API. None of it needs anything from you yet.

## How to upgrade

Check Node first: Nuxt 4.0 needs Node 20.19 or a later 20.x, or Node 22.12 or later (Nuxt 3.17 accepted 20.9 and 22.0). Then run two commands:

```
npx nuxt upgrade --dedupe
npx codemod@latest nuxt/4/migration-recipe
```

- The first updates Nuxt and deduplicates your lockfile; the announcement says this helps pull in updates to the unjs packages Nuxt relies on.
- The second runs the migration recipe from the Codemod registry, which the announcement calls optional. It moves the folders into `app/`, rewrites `=== null` checks on fetched data to `=== undefined`, replaces boolean `dedupe` values and handles the `builder:watch` and template-helper changes for module authors. You can deselect any codemod you do not want to run.

One codemod in the recipe works against the new default: `nuxt/4/shallow-function-reactivity` adds `{ deep: true }` to `useFetch` and `useAsyncData` calls written with a single argument, such as `useFetch('/api/test')`, which keeps Nuxt 3's deep data for them. Deselect it if you want shallow data, and add `deep: true` by hand where a page edits fetched data in place.

Then run your test suite, `nuxt build` and your type check. Fix the TypeScript errors first, then work through the list above for anything that is behaviour rather than types.

**Check it stuck**3 questions from the State: useState, Pinia and cookies quiz.

[**Practice now**](https://vuejs.school/learn/nuxt/state-and-composables?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Release notes · 7 min read**What's new in Nuxt 4.6: nuxt/server, typed $fetch, cookie sessions and Vapor**](https://vuejs.school/community/blog/whats-new-in-nuxt-4-6) [Tutorial · 6 min read**useFetch or $fetch? Picking the right one in Nuxt**](https://vuejs.school/community/blog/usefetch-or-fetch-picking-the-right-one-in-nuxt)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/nuxt-4-is-here#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-07-17","description":"Nuxt 4.0.0 was published on 15 July. A new app/ folder, saner data fetching defaults, split TypeScript projects and a faster CLI, with few surprises.","name":"Nuxt 4.0 is here: the app/ directory, data fetching changes and the upgrade","url":"https://vuejs.school/community/blog/nuxt-4-is-here","breadcrumb":{"@id":"https://vuejs.school/community/blog/nuxt-4-is-here#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/nuxt-4-is-here"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/nuxt-4-is-here#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-07-17","description":"Nuxt 4.0.0 was published on 15 July. A new app/ folder, saner data fetching defaults, split TypeScript projects and a faster CLI, with few surprises.","headline":"Nuxt 4.0 is here: the app/ directory, data fetching changes and the upgrade","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~TnV4dCA0LjAgaXMgaGVyZTogdGhlIGFwcC8gZGlyZWN0b3J5LCBkYXRhIGZldGNoaW5nIGNoYW5nZXMgYW5kIHRoZSB1cGdyYWRlIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~TnV4dCA0LjAgaXMgaGVyZTogdGhlIGFwcC8gZGlyZWN0b3J5LCBkYXRhIGZldGNoaW5nIGNoYW5nZXMgYW5kIHRoZSB1cGdyYWRl%2Cdescription_~TnV4dCA0LjAuMCB3YXMgcHVibGlzaGVkIG9uIDE1IEp1bHkuIEEgbmV3IGFwcC8gZm9sZGVyLCBzYW5lciBkYXRhIGZldGNoaW5nIGRlZmF1bHRzLCBzcGxpdCBUeXBlU2NyaXB0IHByb2plY3RzIGFuZCBhIGZhc3RlciBDTEksIHdpdGggZmV3IHN1cnByaXNlcy4%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9udXh0LTQtaXMtaGVyZSI%2Cs_HlBA7Tz2I1Mf9L9N.png","@type":["Article","BlogPosting"],"articleSection":["Release notes"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/nuxt-4-is-here#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/nuxt-4-is-here#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/nuxt-4-is-here#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Nuxt 4.0 is here: the app/ directory, data fetching changes and the upgrade","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~TnV4dCA0LjAgaXMgaGVyZTogdGhlIGFwcC8gZGlyZWN0b3J5LCBkYXRhIGZldGNoaW5nIGNoYW5nZXMgYW5kIHRoZSB1cGdyYWRlIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~TnV4dCA0LjAgaXMgaGVyZTogdGhlIGFwcC8gZGlyZWN0b3J5LCBkYXRhIGZldGNoaW5nIGNoYW5nZXMgYW5kIHRoZSB1cGdyYWRl%2Cdescription_~TnV4dCA0LjAuMCB3YXMgcHVibGlzaGVkIG9uIDE1IEp1bHkuIEEgbmV3IGFwcC8gZm9sZGVyLCBzYW5lciBkYXRhIGZldGNoaW5nIGRlZmF1bHRzLCBzcGxpdCBUeXBlU2NyaXB0IHByb2plY3RzIGFuZCBhIGZhc3RlciBDTEksIHdpdGggZmV3IHN1cnByaXNlcy4%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9udXh0LTQtaXMtaGVyZSI%2Cs_HlBA7Tz2I1Mf9L9N.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~TnV4dCA0LjAgaXMgaGVyZTogdGhlIGFwcC8gZGlyZWN0b3J5LCBkYXRhIGZldGNoaW5nIGNoYW5nZXMgYW5kIHRoZSB1cGdyYWRlIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~TnV4dCA0LjAgaXMgaGVyZTogdGhlIGFwcC8gZGlyZWN0b3J5LCBkYXRhIGZldGNoaW5nIGNoYW5nZXMgYW5kIHRoZSB1cGdyYWRl%2Cdescription_~TnV4dCA0LjAuMCB3YXMgcHVibGlzaGVkIG9uIDE1IEp1bHkuIEEgbmV3IGFwcC8gZm9sZGVyLCBzYW5lciBkYXRhIGZldGNoaW5nIGRlZmF1bHRzLCBzcGxpdCBUeXBlU2NyaXB0IHByb2plY3RzIGFuZCBhIGZhc3RlciBDTEksIHdpdGggZmV3IHN1cnByaXNlcy4%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9udXh0LTQtaXMtaGVyZSI%2Cs_HlBA7Tz2I1Mf9L9N.png"}]}
```