Nuxt 5 is not out yet. As of September 2026 the latest stable release is 4.5.2 and the Nuxt roadmap puts 5.0 in Q4 2026, estimated. You can still start the Nuxt 5 upgrade today: set future.compatibilityVersion: 5 and most of the Vue-side breaking changes apply immediately. The server-side ones do not.
That split is the whole story. Nuxt 5’s headline change is Nitro v3, and the compatibility flag does not give it to you. Everything else can be fixed on a branch this afternoon.
What does future.compatibilityVersion: 5 actually change?
The flag has been available since Nuxt 4.2. One line in your config:
// nuxt.config.ts
export default defineNuxtConfig({
future: {
compatibilityVersion: 5,
},
})
That flips a set of defaults to their v5 values. The upgrade guide lists them all; three of them will find real bugs in an existing app.
Routing becomes case-sensitive, matching how Nitro has always behaved. A link to /AboutUs pointing at pages/aboutus.vue used to work and now 404s. Fix the links, or set router.options.sensitive: false and deal with it later.
Typed pages are on by default: experimental.typedPages is no longer experimental, so navigateTo('/prodcut/1') becomes a type error rather than a runtime surprise. If your routes are built dynamically you will need to extend the generated route types instead of fighting them.
And the Vue Options API is compiled out of the client bundle. Good for bundle size, bad if a dependency you have not looked at in two years still uses data() and methods. Turn it back on with vue: { optionsApi: true } while you audit.
The rest are smaller. process.server type augmentation is gone, so use import.meta.server. callHook may return void instead of a promise, so await it rather than chaining .then(). Client-only components render an HTML comment placeholder instead of a <div>, which fixes a scoped-styles hydration bug and breaks any layout that was relying on that div existing. The generated tsconfig turns on noUncheckedSideEffectImports, so import './styles.css' needs an ambient declare module '*.css' {}, matching the TypeScript 7 default.
What does Nitro 3 break in server code?
This is the part you cannot test with the flag, and the part that will take the time. Nitro v3 is a rewrite on top of srvx and h3 v2, built around web standard Request, Response, Headers and URL objects.
Auto-imports in server/ keep working. defineEventHandler, getQuery, readBody and useRuntimeConfig are all still there without an import statement. What changes is explicit imports, error construction and everything you reach for on the event object:
// server/api/order.post.ts
// Nitro 2 / h3 v1
import { createError, defineEventHandler, getHeader } from 'h3'
export default defineEventHandler(async (event) => {
const key = getHeader(event, 'x-api-key')
if (!key) {
throw createError({ statusCode: 401, statusMessage: 'Missing key' })
}
const config = useRuntimeConfig(event)
event.node.res.statusCode = 201
return { path: event.path, method: event.method, region: config.region }
})
// server/api/order.post.ts
// Nitro 3 / h3 v2
import { defineEventHandler, HTTPError } from 'nitro/h3'
export default defineEventHandler(async (event) => {
const key = event.req.headers.get('x-api-key')
if (!key) {
throw new HTTPError({ status: 401, statusText: 'Missing key' })
}
const config = useRuntimeConfig()
event.res.status = 201
return {
path: event.url.pathname,
method: event.req.method,
region: config.region,
}
})
Four separate renames are hiding in there. Imports move from h3 to nitro/h3, and the nitropack package is now nitro (nitropack/types becomes nitro/types, which matters if your module augments NitroRouteRules). Error properties follow the web spec: statusCode and statusMessage become status and statusText. useRuntimeConfig() no longer takes the event. And event.node.{req,res} is Node-only now, with event.req being an actual Request.
Two more that are easy to miss because they live in config rather than handlers. Redirect route rules rename their status field:
routeRules: {
'/old-page': { redirect: { to: '/new-page', status: 302 } },
}
And Cloudflare bindings move off the event context. event.context.cloudflare.env becomes event.req.runtime.cloudflare.env, per the Nitro migration guide. Preset names shift too: cloudflare and cloudflare_worker collapse into cloudflare_module, node becomes node_middleware, vercel-edge is replaced by vercel with Fluid compute, and edgio, cli and service_worker are gone.
One subtle trap for error handling in the Vue half of the app. createError from Nuxt still works and is still the right thing to call, but NuxtError is no longer a subclass of h3’s HTTPError, so error instanceof HTTPError stops matching. Use HTTPError.isError(error) for both, or isNuxtError(error) from #app when you specifically want Nuxt’s.
Can you test the Nitro 3 changes early?
Not properly, and it is worth knowing why before you plan a sprint around it. Nitro 3 is still on a beta tag on npm (3.0.260610-beta at the time of writing) and h3 v2 is on a release candidate. Nuxt 4.5.2 ships @nuxt/nitro-server, which still depends on nitropack 2.13.x and h3 1.15.x. The compatibility flag changes app-layer defaults; it does not swap the server engine underneath you.
So treat the server work as preparation rather than migration. Stop reaching into event.node where a web-standard equivalent exists. Read headers through a small helper of your own rather than calling getHeader in forty files. Keep createError calls in one factory function. Every one of those is a single-file change later instead of a repository-wide find and replace.
The Nuxt docs are blunt about the state of it: “We are still working on Nitro v3 integration so you should expect further changes.”
Why does removing jiti affect nuxt.config.ts?
Nuxt 5 drops jiti as a bundled dependency. Files loaded outside the bundler, meaning nuxt.config.ts, anything in modules/, and layer configs, are imported by the Node runtime directly. That is why Nuxt 5 requires Node 22.19 or later, where type stripping is on by default.
Node will not guess file extensions and will not compile TypeScript that emits runtime code. So relative imports in those files need real extensions (./build/my-plugin.ts, not ./build/my-plugin, reported as TS2835), and the same files must use erasable syntax only: no enum, no namespace, no constructor parameter properties, no decorators, all flagged as TS1294.
If you publish a Nuxt module or layer, this one is not optional. Node refuses to strip types inside node_modules at any version, so a TypeScript entrypoint cannot load natively and every consumer has to install jiti to use your package. Ship compiled JavaScript, and emit nuxt.config.mjs if your package has one. Inside your own project, layers/*/nuxt.config.ts still loads fine. If something in your setup genuinely needs jiti, it is now an optional peer dependency and Nuxt picks it up automatically once installed.
Is Vite 8 part of this?
No, and the upgrade guide is slightly behind the releases here. Nuxt 4.5 already moved to Vite 8: @nuxt/vite-builder 4.5.2 depends on vite ^8.2.0, along with Rspack 2 on the other builder. So the Rolldown-related work, vite.esbuild becoming vite.oxc, build.rollupOptions becoming build.rolldownOptions, and the stricter CommonJS interop, lands with your next 4.x bump rather than with 5.0. We wrote up what breaks in a Vite 8 upgrade separately.
When is the Nuxt 5 upgrade worth starting?
Now, if you are on Nuxt 4.2 or later: run npx nuxt upgrade --dedupe, set the compatibility flag on a branch, and fix whatever falls over. It is a day of work on most apps and it is work you cannot avoid.
Do not put Nitro 3 in front of a client yet. It is beta, the Nuxt integration is explicitly unfinished, and the migration guide is described by its own authors as a living document. Read it, budget for it, and write your server code so the eventual change is small.
If you are still on Nuxt 3, none of this is your next move. Nuxt 3 reached end of life on 31 July 2026. Get to Nuxt 4 first, then take the flag. Nuxt 4 itself is supported for at least six months after 5.0 ships, so there is no cliff waiting in Q4.