Nuxt 4.6 is here, and it brings Nuxt CLI v4 with it. It is one of our biggest minor releases, with over 420 commits since v4.5.2, and it marks our first big step towards a server-agnostic Nuxt.
๐ฃ Some News
Nuxt CLI v4
Alongside Nuxt 4.6, today also brings a new major release of the Nuxt CLI: @nuxt/cli v4. It ships as a dependency of nuxt, so you'll get it automatically when you upgrade. Most of what's new is in nuxt dev.
There's now an interactive panel pinned to the bottom of your terminal with URLs, startup progress and single-key shortcuts (r to restart, o to open, l for logs, n for requests, p for pages and server routes). Every request gets an id, so logs and errors are attributed to the request that caused them. If you prefer the classic output, pass --no-tui.
The dev server also tells you why it reloaded or restarted, which nuxt.config keys changed, where the time went during a slow start, and how long each module took to set up. A save that changes nothing no longer restarts the server, and hard restarts keep serving on the same port. Errors go through a single CLI-level channel rendered with my-bad (more below).
A lock file in .nuxt/ lets a second nuxt dev (say, one started by an agent) take over from or defer to the one you started. The same lock file powers new nuxt curl and nuxt task commands that talk to the running server, and nuxt docs "<query>" searches the docs for your installed Nuxt version. You can also use nuxt preview --takeover to replace a running preview server.
It's also a lot smaller and starts a lot faster:
| v3.37 | v4.0 | ||
|---|---|---|---|
@nuxt/cli install size | 13.1 MB | 3.5 MB | -73% |
@nuxt/cli dependencies | 70 | 31 | -56% |
nuxt dev: first paint | 330 ms | 50 ms | 6.6x faster |
nuxt dev: port bound | 338 ms | 104 ms | 3.2x faster |
nuxt dev: memory at rest (Linux) | 630 MB | 440 MB | -30% |
Although this is a major version, none of the changes should be breaking for Nuxt v4 users: the CLI requires Node.js v22.21+, v24.11+ or v26+, nuxt init has been dropped in favour of npm create nuxt@latest, and Nuxt 2 and @nuxt/bridge are no longer supported.
A Server-Agnostic Nuxt
The biggest thing about this release is our move towards making Nuxt server-agnostic.
Freedom of choice is a fundamental value of the web, and one that unites the whole Nuxt team. You can use pages/ (with vue-router) or not. You can use Vite, webpack or Rspack to bundle your code. You can pick from dozens of providers to deploy to, pick any image or font provider, choose any database adapter. In every case, the framework is the same.
The server side was different. #app composables imported h3 types, server code imported from h3 and nitropack, and every module that touched the server was tied to whichever major version of those packages Nuxt happened to depend on. This has become particularly clear as we have been upgrading to new majors of h3 and Nitro, which ship breaking changes with a cascading effect throughout the whole ecosystem.
This release changes that. Alongside explicitly defining our public API in @nuxt/kit (which now does not refer to external packages), Nuxt now specifies its own types for the request event, route rules and typed $fetch, and exposes an import surface, nuxt/server, for the server utilities most apps need. It is the culmination of work we started almost a year ago to make it possible to use any server builder with Nuxt, not just Nitro (#33462).
Under the hood, nuxt/server is still powered by Nitro by default. We are also announcing a second, experimental implementation, @nuxt/vite-server, which allows pure-Vite server builds using the Vite Environment API.
We believe that Nitro is still the right choice for almost everyone.
We see a number of key benefits for nuxt/server:
- It smooths the upgrade to Nuxt 5, which moves to Nitro v3 and h3 v2. Server code written against
nuxt/serveron 4.6 runs unchanged there, so a module can ship one file for both. - It decouples Nuxt from the Nitro release cycle. Because we own the API, we can adapt to breaking changes in Nitro or h3 without requiring a new major, and we can release Nuxt majors without waiting for upstream releases.
- It makes Nuxt's code more maintainable. It preserves the separation of concerns between our API (
nuxt/appandnuxt/server) and the bundler and server you ultimately build your app with.
There are other benefits too, from a single type surface to being able to iterate more quickly on features.
Finally, a special thank-you to @pi0, whose relentless focus on server agnosticism and work on h3, Nitro and web-standard server primitives over the last few years is what makes a portable RequestEvent possible at all. Thank you, Pooya. โค๏ธ
Almost every feature in this release is already on the Nuxt 5 branch, and you can try most of the remaining Nuxt 5 defaults today with future.compatibilityVersion: 5 (more below).
Nuxt 3 End-of-Life
Nuxt 3 reached end-of-life on July 31, 2026, so there is no 3.x release alongside this one. If you're still on v3, the upgrade guide is waiting for you.
If you've read this far, I'm afraid I have bad news for you: there's a lot more still to say. You might want to grab a coffee. โ๏ธ
๐งฉ nuxt/server
It has been asked for for a long time, and it now exists (#36275)! ๐
nuxt/server is a new import source for server code: handlers, middleware and utilities. It is the complement to nuxt/app, which is for the part of your application that also runs in the browser.
server/api/hello.ts
import { defineEventHandler, getQuery } from 'nuxt/server'
export default defineEventHandler((event) => {
const { name } = getQuery<{ name?: string }>(event)
return { message: `Hello, ${name ?? 'world'}!` }
})The utilities use web standards and are typed against a portable RequestEvent:
event.req // Request
event.url // URL
event.res // { status, statusText, headers }
event.res.headers // Headers
event.context // per-request contextUnder @nuxt/nitro-server these are implemented with Nitro and h3, but your code doesn't import from either. So the same handler runs under Nitro v2, Nitro v3 or @nuxt/vite-server, and a module that imports from nuxt/server doesn't need a peer dependency on h3 or nitropack. We think this will make a big difference in smoothing out the upgrade to Nuxt v5 and Nitro v3.
Types get simpler too. We no longer hoist h3 or Nitro types into your app to type useRequestEvent, $fetch or route rules, which removes a source of conflicts when versions differ.
The surface is small, and covers what published modules and user code typically need: defineEventHandler, createError/isNuxtError, request URL, headers, query, body (plain, or validated with any Standard Schema library or a function), cookies, redirects, response status, getRouterParam(s), getRequestIP, handleCors, getRouteRules, useRuntimeConfig, useAppConfig and sessions.
server/api/users.post.ts
import { defineEventHandler, readValidatedBody } from 'nuxt/server'
import { z } from 'zod'
export default defineEventHandler(async (event) => {
const user = await readValidatedBody(event, z.object({ name: z.string() }))
return { created: user.name }
})We encourage you to use web APIs (event.req.headers, for example), or to raise an issue if there's functionality you're missing from nuxt/server. ๐
If you do need to step outside nuxt/server for a particular handler, don't worry. Nothing has been taken away: import defineEventHandler and the helpers you need from h3 or nitropack/runtime as before, and that handler works exactly as it did on Nuxt 4.5.
server/api/upload.post.ts
import { defineEventHandler, readMultipartFormData } from 'h3'
export default defineEventHandler(async (event) => {
const parts = await readMultipartFormData(event)
return { received: parts?.length ?? 0 }
})On Nuxt 4, the auto-importeddefineEventHandler, getQuery and readBody (and the rest of that family) are still h3's own helpers, which take a different shape of event. Import from nuxt/server explicitly, including defineEventHandler, to use the new server runtime. If you mix the two, you'll see a NUXT_E8012 error which should tell you which import to change.
A few helpers also behave differently from their h3 v1 namesakes: sendRedirect returns the response rather than sending it, createError takes status and statusText, and response headers are set through event.res.headers. The upgrade guide has a table of the differences.
Nothing in this release requires a migration. But if you have server code you'd like to make portable ahead of Nuxt 5, this is the best way.
๐ appSecret and Sessions
Thanks to @onmax, Nuxt now has a root application secret: runtimeConfig.appSecret, set with NUXT_APP_SECRET (#35874). Modules and server features derive their own secrets from it with deriveSecret(purpose), so it's the only secret you need to configure.
In development, Nuxt generates and persists one if none is configured, and warns the first time a derived secret is used. Builds never generate one.
The first feature to use it is a set of session helpers in nuxt/server (#36358). Sessions are sealed into a cookie with iron, so there's no server-side storage to set up:
server/api/visits.ts
import { defineEventHandler, useSession } from 'nuxt/server'
export default defineEventHandler(async (event) => {
const session = await useSession<{ visits: number }>(event)
await session.update(data => ({ visits: (data.visits ?? 0) + 1 }))
return { visits: session.data.visits }
})As a reminder, runtime config keys prefixed with app (runtimeConfig.app, runtimeConfig.appSecret) are reserved for Nuxt.
๐ฏ Typed $fetch, Rebuilt
$fetch and useFetch have been typed from your server routes for a long time. The types came from Nitro's InternalApi interface, and past a few hundred routes they hit TypeScript's instantiation limit:
error TS2589: Type instantiation is excessively deep and possibly infinite.We've rebuilt typed fetch on top of fetchdts (#36238). Nuxt compiles your server routes into a route tree with an exact-match table for static paths and accessors specialised to your route set. Resolution cost now scales with call sites, not with route count:
| routes | before | after |
|---|---|---|
| 100 | 1,397,361 instantiations / 0.77s | 51,558 / 0.26s |
| 300 | 5,774,425 / 3.49s (TS2589) | 51,558 / 0.16s |
| 1000 | 12,831,467 / 7.44s (TS2589) | 51,558 / 0.20s |
| 3000 (200 call sites) | 71,875,148 / 53.63s (TS2589) | 101,678 / 0.62s |
Peak memory for the same runs dropped from 946 MB to 140 MB. ๐ฅ
Plus, the route set also carries the body, query and headers each handler validates, so calls are checked against them:
// server/api/users.post.ts validates { title: string, count: number }
await $fetch('/api/users', { method: 'post', body: { title: 'a', count: 1 } })
await $fetch('/api/users', { method: 'post', body: { title: 'a', count: 'no' } })
// ^ not assignable to number
await $fetch('/api/users', { method: 'post' })
// ^ body is required

On Nuxt 4 this is opt-in, because there are small changes to type inference (a Date returned by a handler is a string at the call site, for example), and hand-written ServerRoutes augmentations need a small rewrite. It's the default in Nuxt 5.
nuxt.config.ts
export default defineNuxtConfig({
experimental: {
routeTypedFetch: true,
},
})There's also a new experimental.strictRouteTypes option to reject calls to paths that don't exist (otherwise these just return unknown), and an 'isomorphic' mode that types your pages as GET routes too, if you want to $fetch them from the Vue renderer with type safety.
๐ Better Errors in Development
Server-side errors in development used to look like this:
ReferenceError: foo is not defined
at Object.<anonymous> (/_nuxt/app/pages/index.vue:1:1)There was no source position or code frame, and the Youch iframe we rendered could not show you the frame in your own source either. Both are now fixed (#36258, nuxt/cli#1518).
Dev SSR stack traces are now mapped before anything reads the error, and the Youch overlay has been replaced with my-bad. It renders as an overlay in your app's own error page (or as a standalone page when the app can't render one), with the mapped stack trace and a code frame from your source.

There's a Copy error button with a few formats, including a prompt you can hand straight to an agent:

The same report is printed in your terminal:

With Nuxt CLI v4, errors also go through a single live channel at the CLI level, which survives worker restarts. A syntax error in nuxt.config.ts (or any other fatal startup error) is served as a page, and the page reloads once you fix it. Each error is rendered once, rather than at every layer it passes through.

We're working with @atinux, @HugoRCD and @antfu to make these pages nicer still.
๐จ A New Loading Screen, 404 and Error Pages
@HugoRCD has redrawn the loading screen you see while the dev server starts. It's now a WebGL2 particle field that traces a mountain range behind the Nuxt lockup (#36178), using a single shader and a single draw call. Without WebGL2 it falls back to the static lockup, and with prefers-reduced-motion the animation stops. Try hovering over it. ๐๏ธ
Hugo also gave the built-in 404 and error pages a neutral palette and lighter type (#36255), and @MirkoJa added a back button to the 404 page (#35688).


With experimental.prerenderErrorPages, you can prerender error pages as real HTML (404.html, or any status codes you pass) instead of an empty SPA shell (#35193, thanks to @Flo0806).
โก๏ธ Vue Vapor Support
Nuxt now supports Vue 3.6's Vapor Mode in interop mode (#35759). Your app root stays on the virtual DOM, and you can opt individual components or pages into Vapor with the vapor attribute on <script setup>:
nuxt.config.ts
export default defineNuxtConfig({
vue: {
vapor: true,
},
})app/pages/index.vue
<script setup vapor lang="ts">
const count = ref(0)
</script>
<template>
<button @click="count++">
count is {{ count }}
</button>
</template>Routing, useAsyncData, layouts and most built-in components work without changes. To get there, we taught the auto-import loader, useAsyncData, definePageMeta and slot inspection about Vapor, sent some fixes upstream to Vue, and added a Vapor test suite to track what's supported.
This requires Vue ^3.6.0-rc.2 or newer. See the
, and try it in a fresh project first.
๐ฎ Nuxt 5 Features, Today
Most of what's new in Nuxt 5 is already in 4.6, either on by default or behind a flag. future.compatibilityVersion: 5 turns on the Nuxt 5 defaults at once, and you can still toggle each one individually.
nuxt.config.ts
export default defineNuxtConfig({
future: {
compatibilityVersion: 5,
},
})This release adds these to the flag:
- typed pages (
experimental.typedPages) - typed
$fetch(experimental.routeTypedFetch), described above - case-sensitive routing, matching Nitro (#35650, thanks to @Mateleo)
experimental.navigateToEarlyReturn: callingnavigateToin<script setup>skips the rest of setup, so redirects and 404s don't throw on missing data- build-time extraction of serializable
definePageMetakeys (experimental.extractSerializablePageMeta, #35919) experimental.payloadExtraction: 'client'- no
baseUrlin generated tsconfigs (#36040, thanks to @oritwoen) - inline error rendering (
experimental.inlineErrorRendering): when a server render fails,error.vueis rendered in the same request with a plain try/catch instead of an internal request to/__nuxt_error, so headers and cookies already set are kept, the error render no longer passes through Nitro middleware and route rules a second time, andrender:htmlfires with the original event (#36399) - normalized page names,
clearNuxtStateresetting to defaults, andexperimental.watcher: 'builder' - no auto-imported server-only head composables (
useServerHead,useServerHeadSafeanduseServerSeoMeta)
The upgrade guide lists exactly what the flag changes on Nuxt 4.
๐งช Experimental: @nuxt/vite-server
server.builder has been configurable since v4.2. This release adds a second server builder: @nuxt/vite-server, which builds a Nuxt app with Vite alone (#36218, #36279, #36288).
This is all you need to configure it:
nuxt.config.ts
export default defineNuxtConfig({
server: {
builder: 'vite', // 'nitro' is the default
},
})It can produce a client-only SPA, a server-rendered app with a small Node entry, a web-standard fetch handler for platforms that provide the server (there are e2e examples for Cloudflare Workers, Netlify and universal deploy), as well as fully static output with nuxt generate.
Right now, this helps keep Nuxt's code agnostic, enforces the contract behind nuxt/server, and gives Vite plugins that provide a deploy target something to build on. It doesn't have Nitro's full feature set (there is no storage, caching, tasks or server plugins), and we expect most apps to keep using Nitro. Nitro remains the default.
This is highly experimental and the API will change. 'nitro' and 'vite' are new shorthands for @nuxt/nitro-server and @nuxt/vite-server.
๐ Performance
Here's how 4.6 compares to 4.5.2 on our benchmark machine (arm64 Linux, Node.js 24.15, medians of five runs):
| v4.5.2 | v4.6.0 | ||
|---|---|---|---|
@nuxt/kit install size | 6.4 MB | 2.1 MB | -67% |
@nuxt/kit transitive dependencies | 36 | 22 | -39% |
nuxt build, starter | 4.4 s | 3.6 s | -18% |
nuxt build, 200 pages / 200 components / 50 routes | 11.6 s | 10.3 s | -11% |
SSR throughput, page with 300 <NuxtLink>s | 110 req/s | 140 req/s | +26% |
Dev server start-up (spawn to first HTML) is about 12% faster on a starter app, and the second request takes roughly half the time. The CLI major changed at the same time, so not all of that is Nuxt. Large apps start about as fast as on 4.5.2.
The <NuxtLink> number comes from #36015. Internal links now render on the server as a plain <a>, with no useLink and no reactive state. Rendering 200 links went from 1.36ms to 0.57ms, which is faster than a bare <RouterLink>.
With experimental.early404 (#36117), page routes are compiled into a static matcher at build time. A request that can't match any page skips creating the Vue app and running plugins and middleware. On an app with 30 pages and about 23ms of boot work per render, a JSON 404 went from 37.1ms to 0.3ms.
Templates now declare what invalidates them (#35875). Before, every file change regenerated all of them. Now editing a component regenerates 0 of 49 core templates, and the builder:watch hook settles in 0.6ms instead of 36ms.
useCookie parses the cookie header once per request, or once per microtask on the client (#36114). This also means a cookie set in a plugin during SSR can be read by a later useCookie in a page.
Smaller changes: ssr: false pages are tree-shaken from the server bundle (#35836, thanks to @Austin1serb), unctx is no longer shipped to the browser and defu is skipped for a single app.config (#36371), Vite warms the server module graph before the client, crawls the client graph during warm-up, and yields to your first navigation (#36154, #35870, #36412), and @nuxt/kit has dropped c12, untyped, confbox, pkg-types, ufo and mlly, with jiti and giget now optional peers.
Lighter Payloads
useAsyncData and useFetch accept serialize: false to keep data out of the payload (#35779). This is mostly useful inside components that never hydrate, and experimental.stripNeverHydratedData applies it automatically within hydrate-never component trees.
const { data } = await useFetch('/api/stats', { serialize: false })In development, Nuxt now warns when a page's payload exceeds 100 kB, and when a route rendered with noScripts relies on client-side JavaScript. These pages also keep their non-script resource hints and styles (#35803, #36356, #36359). <NuxtLink> also prefetches server page islands, and @atinux made prefetch hints throttled and prioritised so a page full of links doesn't flood the network (#36261, #36324). Building on that, route chunks, layouts, middleware, payloads, islands and resource hints now all go through one client prefetch scheduler, with per-kind concurrency caps, deduplication by key and cancellation of in-flight work when you navigate away (#36391).
๐งฉ Addons for useFetch and useAsyncData
@cernymatej has added an addons option to the createUseFetch and createUseAsyncData factories (#35797). An addon can declare custom call-site options, adjust the merged options, wrap the handler with middleware, and extend what the composable returns, and it can be reused across as many custom instances as you like.
app/composables/useApiFetch.ts
const refreshOnFocus = defineUseFetchAddon({
setup: (options: UseFetchAddonOptions<{ refreshOnFocus?: boolean }>) => {
if (import.meta.server || !options.refreshOnFocus) { return }
return (asyncData) => {
const focused = useWindowFocus()
watch(focused, focused => focused && asyncData.refresh())
return { focused }
}
},
})
export const useApiFetch = createUseFetch({ baseURL: '/api', addons: [refreshOnFocus] })app/pages/todos.vue
<script setup lang="ts">
const { data, focused } = await useApiFetch('/todos', { refreshOnFocus: true })
</script>This resolves a long list of feature requests for useAsyncData and useFetch (refresh on focus, polling, retries, auth headers and more) without making the core composables opinionated about any of them.
๐ ๏ธ Developer Experience
File paths in terminal output are now clickable (#35898). A warning that mentions a file links to it in your editor, at the relevant line when we know it.

Hovering a built-in component like <NuxtLayout> or <NuxtLink> in a template now shows a short description and a link to its docs (#35701, thanks to @Ibochkarev).

Other changes:
- There's a new top-level
prerenderoption, an alias fornitro.prerenderin the same wayruntimeConfigandrouteRulesare top-level (#32356). Nuxt now also nudges you towards top-level options where they exist, since those work across server builders (#36416). - Linking to a file in
public/with<NuxtLink>(or from Markdown with Nuxt Content) no longer needsexternal: if the router has no route for the path, Nuxt falls through to a full-page load instead of rendering your 404 (#36169). - Renaming a component in development refreshes its imports, so you no longer see stale references to the old file (#36165, thanks to @oritwoen).
- Layers installed from
node_moduleshave their dependencies pre-bundled by Vite (#36208), and symlinked layer directories resolve to their real path (#36402, thanks to @silverbackdan). app/types/andserver/types/are included in the right tsconfig, so ambient types and augmentations there are picked up (#35783). @Flo0806 did this, and also added an augmentableNuxtPageMetainterface for typingNuxtPage.meta(#34816).- A
(group)/folder insidecomponents/is left out of the component name (#35699, thanks to @abaza738). useCookieaccepts a function forexpires(#35628, thanks to @DarlanPrado).- Nuxt warns when a
public/file shadows an application route (#35674, thanks to @Norbiros). typescript.tsConfigis now the shared baseline for every generated tsconfig, withappTsConfigandserverTsConfigfor per-context overrides (#35697, thanks to @chairulakmal).- @DamianGlowala exported the
$Fetchtype fromnuxt/app(#35625) and addedShallowRefto the Vue auto-import preset (#36266).
If you maintain a module with server code, you can now make it compatible with both Nuxt 4 and Nuxt 5 without a major bump. (We'll also be opening PRs proactively after the release of Nuxt 4.6 to help modules prepare for Nuxt 5.)
addServerHandler, addDevServerHandler and addNitroPlugin accept a map of variants per server API (#36317), and Nuxt chooses the most appropriate one:
module.ts
addServerHandler({
route: '/api/my-module/status',
handler: {
nuxt: resolve('./runtime/server/status'),
nitro2: resolve('./runtime/server/status.legacy'),
},
})A handler that imports only from nuxt/server doesn't need Nitro 2/3 variants at all (but does require Nuxt 4.6+). Nuxt reads the file's imports to work out which API it uses, and where that isn't enough you can declare meta.compatibility.server. getNitroVersion and hasNitroVersion are also there in case you have logic that explicitly needs to know which Nitro version is installed (#36127).
Other additions for module authors:
- augmentable server types owned by Nuxt:
ServerTypes,ServerRoutes,AppRouteRulesandNuxtRequestContext(#36293) deriveSecret, for a module-specific secret derived fromappSecretuseTerminal, for prompts, status messages and task progress that the CLI's dev panel renders when there is one (#36162)module:beforeandmodule:donehooks, which Nuxt CLI v4 uses to show per-module setup time (#36173)onConfigResolvedanddiffNuxtConfig, to see what changed between two config loads (#35853)ensureDependencyInstalledandgetAddDependencyCommand, to offer optional dependencies using the user's package manager (#34554)- template
dependencies, to say what should invalidate a template (#35875) addServerImports,addServerImportsDirandaddServerTemplateacross Nitro versions, with generated server tsconfigs and versioned route config types (#36265)- an explicit public API for
@nuxt/kit(#36074), with@nuxt/schemanow an optional peer - modules can set
experimental.asyncContext(#36175, thanks to @cernymatej) - Vite plugins added via kit are registered at the top level (#36037), and more build-time warnings use diagnostic codes (#36138)
- wider
@nuxt/kitpeer dependency ranges (#36417), andupdateRuntimeConfigcan be called before Nitro exists without a warning (#36403, thanks to @Neekoras)
๐ฉน Fixes and Security
@oritwoen fixed scoped styles in server component slots, duplicate island asset requests and blank remounts with lazy hydration (#36047, #36048, #36051). @hdwebpros made useRequestFetch forward request headers (#36180), @Askerka00 fixed view transitions that are skipped mid-navigation (#35537), @Flo0806 made changed auto-import sources rescan before their consumers (#33671), and @onmax preserved error causes in development (#35632).
Other fixes cover awaited useAsyncData during hydration and lazy fetching (#36124, #36301), navigateTo path encoding and baseURL with open (#36055, #36112, #36197), CSS extraction and inlined styles, and interrupted view transitions (#36314).
On the security side, the internal error route can only be reached by an error render, dev error reports are scoped for remote peers and kept out of production builds, and unhandled error data is no longer passed to the error page. The release notes have the details.
โฌ๏ธ Upgrading
Our recommendation for upgrading is to run:
npx nuxt upgrade --dedupeThis will refresh your lockfile and pull in the latest dependencies Nuxt relies on, including Nuxt CLI v4.
This release requires Node.js ^22.22.3 || ^24.15.0 || >=26.0.0.
If you have server code you'd like to make portable, read Moving to nuxt/server in the upgrade guide.
๐ Full Release Notes
Thank you to everyone who contributed to this release. ๐