Nuxt 4.6 came out on 5 October, and the release post calls it one of the biggest minor releases yet: over 420 commits since 4.5.2, shipped alongside a new major of the command line tool, Nuxt CLI v4. Its main theme is making Nuxt server-agnostic, so server code no longer has to depend on the exact versions of Nitro and h3, the libraries that run Nuxt's server.
Nothing in 4.6 requires a migration, but it raises the minimum Node version. The biggest changes are for server routes: a nuxt/server import that carries over to Nuxt 5, and sessions stored in a cookie. The examples use the app/ directory for Vue code and server/ at the project root.
Write server routes with nuxt/server
nuxt/server is a new import source for handlers, middleware and utilities. It is the server counterpart of nuxt/app. The helpers use web standards and receive a portable RequestEvent: event.req is a standard Request, event.url a URL, and event.res.headers a standard Headers object you write response headers to.
This route adds a note for the signed-in user. The session it reads is set by the sign-in route in the next section:
import { createError, defineEventHandler, readValidatedBody, useSession } from 'nuxt/server'
import { z } from 'zod'
const NewNote = z.object({ text: z.string().trim().min(1).max(500) })
export default defineEventHandler(async (event) => {
const session = await useSession<{ userId?: string }>(event)
if (!session.data.userId) {
throw createError({ status: 401, statusText: 'Sign in to add notes' })
}
const note = await readValidatedBody(event, NewNote)
event.res.headers.set('cache-control', 'no-store')
return { id: crypto.randomUUID(), text: note.text, author: session.data.userId }
})
readValidatedBody takes any Standard Schema library (Zod, Valibot, ArkType) or a plain function, and rejects invalid input with a 400 whose data.issues lists what failed. Under the default server builder these helpers are still implemented with Nitro v2 and h3 v1, but your file imports from neither. The release post says the same handler will run unchanged on Nuxt 5, which moves to Nitro v3 and h3 v2.
There is one trap: the auto-imported defineEventHandler, getQuery, readBody and friends in server/ are still h3's own helpers, which take a different event. So import from nuxt/server explicitly, defineEventHandler included. If a nuxt/server helper such as useSession receives h3's event, the request fails with a 500 whose message starts with [NUXT_E8012] and names the import to change.
A few helpers also behave differently from their h3 v1 namesakes:
createErrortakesstatusandstatusText, notstatusCodeandstatusMessage.sendRedirectreturns the response body instead of sending it, so writereturn sendRedirect(event, '/login').- Response headers go through
event.res.headersinstead ofsetResponseHeaderand its relatives.
The upgrade guide's Moving to nuxt/server section has the full table.
Store sessions in a sealed cookie
Nuxt now has a root application secret, runtimeConfig.appSecret, set with the NUXT_APP_SECRET environment variable. Features and modules derive their own secrets from it, so it is the only secret you configure. Generate one with at least 32 random bytes:
openssl rand -base64 32
In development Nuxt generates and keeps one for you if none is set. Builds never generate one: without the variable, useSession fails in production with a 500 that names NUXT_APP_SECRET.
The first feature built on it is a set of session helpers. The session data is encrypted and signed ("sealed") into a cookie with the iron library, so there is no server-side storage to set up. Here is the sign-in route that the notes handler above relies on. It stores the name it receives as userId for a week:
import { defineEventHandler, readValidatedBody, useSession } from 'nuxt/server'
import { z } from 'zod'
export default defineEventHandler(async (event) => {
const { name } = await readValidatedBody(event, z.object({ name: z.string().min(1) }))
const session = await useSession<{ userId?: string }>(event, { maxAge: 60 * 60 * 24 * 7 })
await session.update({ userId: name })
return { signedIn: true }
})
The cookie is named nuxt-session and defaults to httpOnly, secure, sameSite: 'lax' and path: '/'. maxAge is in seconds and expires both the cookie and the sealed value; session.clear() signs the user out. Two limits from the server imports guide:
- A session that serialises to more than 4096 bytes throws, so store an id and keep the rest in a database.
- h3's own
useSession, which the auto-import still resolves to, is a different implementation: a cookie written by one cannot be read by the other.
Typed $fetch that scales to large apps
$fetch and useFetch have long inferred response types from your server routes. Past a few hundred routes, that inference hit TypeScript's limit with error TS2589: Type instantiation is excessively deep and possibly infinite. Nuxt 4.6 rebuilds it on a compiled route tree. The release post's numbers: at 1,000 routes, type checking drops from 12.8 million instantiations and 7.44 seconds to about 51,000 and 0.2 seconds, and peak memory in their runs falls from 946 MB to 140 MB.
The new types also check the body, query and headers a handler validates, so a missing or wrongly typed body is a compile error at the call site. On Nuxt 4 it is opt-in:
export default defineNuxtConfig({
experimental: {
routeTypedFetch: true,
},
})
It is opt-in because inference changes slightly. A Date returned by a handler is typed as a string at the call site, which is what JSON actually delivers, and hand-written ServerRoutes augmentations need rewriting. A companion option, experimental.strictRouteTypes, rejects calls to paths that do not exist instead of typing them as unknown.
One catch on Nuxt 4: Nitro still declares a global $fetch with the old types, so import $fetch from #build/fetch where you want a direct call checked. useFetch gets the new types either way.
Opt components into Vapor Mode
Nuxt 4.6 supports Vapor Mode, Vue 3.6's new way of compiling components without the virtual DOM, in interop mode. The app root stays on the virtual DOM, and you opt single components or pages in with the vapor attribute:
export default defineNuxtConfig({
vue: {
vapor: true,
},
})
Then write <script setup vapor lang="ts"> in the component you want compiled that way. The release post says routing, useAsyncData, layouts and most built-in components work unchanged, and that Nuxt added a Vapor test suite to track what is supported. The Nuxt docs list the known limitations, such as no Options API inside Vapor components.
Clearer errors and Nuxt CLI v4 in development
Server errors in development now come with source-mapped stack traces and a code frame from your own file. The old Youch overlay is replaced by my-bad, which renders inside your app's error page and offers a "Copy error" button, including a format meant to paste into an AI assistant.
Nuxt CLI v4 comes as a dependency of nuxt, so you get it with the upgrade. Most of what changes is in nuxt dev:
- A panel pinned to the bottom of the terminal has single-key shortcuts:
rrestarts,oopens the browser,lshows logs,nrequests,ppages and server routes. Pass--no-tuifor the classic output. nuxt devsays why it restarted, and a save that changes nothing no longer restarts at all.- New commands
nuxt curlandnuxt tasktalk to the running dev server, andnuxt docs "<query>"searches the docs for your installed version.
The release post measures the install at 3.5 MB instead of 13.1 MB and the first paint in nuxt dev at 50 ms instead of 330 ms. nuxt init is gone in favour of npm create nuxt@latest.
Lighter payloads, fetch addons and redrawn pages
Three smaller changes:
serialize: falseonuseFetchanduseAsyncDatakeeps the data out of the payload (what the server sends to the browser with the page), which is mostly useful in components that never hydrate.createUseFetchaccepts addons, written withdefineUseFetchAddon, for behaviour like refresh on focus or retries.- The built-in loading screen, 404 page and error page were redrawn, and the 404 page has a back button.
How to upgrade
- Check Node. Nuxt 4.6 requires Node
^22.22.3,^24.15.0or>=26.0.0, a higher floor than 4.5's^22.19.0and^24.11.0. Update your CI image and your hosting platform's Node setting before you merge: on an older Node, npm only prints an engine warning unlessengine-strictis set, so the mismatch is easy to miss. - If you plan to use sessions, set
NUXT_APP_SECRETon every environment. - Run the command the release post recommends, which also refreshes your lockfile and pulls in Nuxt CLI v4:
npx nuxt upgrade --dedupe
After that, nothing else is required. If you are still on Nuxt 3, there is no 3.x release this time: Nuxt 3 reached end of life on 31 July.
The release post gives no date for Nuxt 5, but says most of what is new in it is already in 4.6. To turn on those Nuxt 5 defaults together, typed fetch and typed pages among them, set future.compatibilityVersion: 5 in nuxt.config.ts.