Upgrade Guide
Upgrading Nuxt
Latest release
To upgrade Nuxt to the latest release, use the nuxt upgrade command.
npx nuxt upgrade
yarn nuxt upgrade
pnpm nuxt upgrade
bun x nuxt upgrade
deno x nuxt upgrade
Nightly Release Channel
To use the latest Nuxt build and test features before their release, read about the nightly release channel guide.
Testing Nuxt 5
Nuxt 5 is currently in development. Until the release, it is possible to test many of Nuxt 5's breaking changes from Nuxt version 4.2+.
Opting in to Nuxt 5
First, upgrade Nuxt to the latest release.
Then you can set your future.compatibilityVersion to match Nuxt 5 behavior:
export default defineNuxtConfig({
future: {
compatibilityVersion: 5,
},
})
When you set your future.compatibilityVersion to 5, defaults throughout your Nuxt configuration will change to opt in to Nuxt v5 behavior, including:
- Vite Environment API: Uses the new Vite Environment API for improved build configuration
- Case-sensitive routing: Page routes match URL casing exactly, consistent with Nitro
- Normalized Page Names: Page component names will match their route names for consistent
<KeepAlive>behavior clearNuxtStateresets to defaults:clearNuxtStatewill reset state to its initial value instead of setting it toundefined- Non-async
callHook:callHookmay returnvoidinstead of always returning aPromise - Comment node placeholders: Client-only components use comment nodes instead of
<div>as SSR placeholders, fixing a scoped styles hydration issue - Stricter side-effect imports: The generated
tsconfig.jsonenablesnoUncheckedSideEffectImportsto match the TypeScript 7 default - Vue Options API disabled: The Options API is compiled out of the client bundle to reduce its size
process.*type augmentation removed: TypeScript no longer exposes deprecatedprocess.*flags onNodeJS.Process- Typed pages:
experimental.typedPagesis enabled by default for type-checked routing - TypeScript
baseUrlignored: Generated TypeScript configurations no longer usecompilerOptions.baseUrlto resolve Nuxt aliases - Other Nuxt 5 improvements and changes as they become available
future.compatibilityVersion: 5.Breaking or significant changes will be noted below along with migration steps for backward/forward compatibility.
process.* Type Augmentation Removed
๐ฆ Impact Level: Minimal
What Changed
Nuxt no longer augments NodeJS.Process with browser, client, dev, server, and test. Build-time defines for those flags remain for compatibility, but TypeScript will not treat them as known properties.
Prefer import.meta.*. Those flags are replaced at build time and stay tree-shakeable.
Migration Steps
Replace legacy checks in your app code, modules, and libraries:
// Before
// eslint-disable-next-line nuxt/prefer-import-meta
if (process.server) {
/* ... */
}
// After
if (import.meta.server) {
/* ... */
}
The nuxt/prefer-import-meta ESLint rule flags remaining process.* usage.
Case-Sensitive Routing
๐ฆ Impact Level: Minimal
What Changed
With compatibilityVersion: 5, page routes match URLs case-sensitively, consistent with Nitro. For example, /About no longer matches pages/about.vue.
Migration Steps
Update links to use the same casing as their page routes. To keep case-insensitive matching:
export default defineNuxtConfig({
router: {
options: {
sensitive: false,
},
},
})
jiti Is No Longer Bundled
๐ฆ Impact Level: Medium
What Changed
Nuxt no longer depends on jiti. Files loaded outside the bundler (nuxt.config.ts, files in modules/, and layer configs) are now imported by the runtime itself.
You can still write your config in TypeScript. Nuxt 5 requires Node 22.19 or later, where type stripping is on by default, so nuxt.config.ts and TypeScript modules load natively. Two things the runtime does not do, which jiti used to paper over, are guess file extensions and compile TypeScript syntax that emits code.
Both would otherwise only surface when Nuxt loads the file, so the generated node tsconfig now describes the environment the way the runtime sees it (module and moduleResolution set to nodenext, plus erasableSyntaxOnly) and TypeScript reports both up front.
That covers nuxt.config, modules/ and layer configs only. Your app and shared/ code goes through Vite and resolves the way it always has. If you need the previous behaviour, override it:
export default defineNuxtConfig({
typescript: {
nodeTsConfig: {
compilerOptions: {
module: 'preserve',
moduleResolution: 'bundler',
erasableSyntaxOnly: false,
},
},
},
})
Reasons for Change
jiti was pulled into every Nuxt install to load a handful of files, most of which the runtime can now load unaided. Removing it makes a default install smaller.
Migration Steps
1. Add file extensions to relative imports.
Relative imports in nuxt.config.ts, modules/, and layer configs need an explicit extension:
- import { myPlugin } from './build/my-plugin'
+ import { myPlugin } from './build/my-plugin.ts'
TypeScript reports a missing extension as TS2835. Note that its quick fix suggests ./build/my-plugin.js; write the extension the file actually has (.ts), which Node resolves directly.
Bare package imports (import { defu } from 'defu') are unaffected.
2. Use erasable TypeScript syntax.
Type annotations are erased, but syntax that emits runtime code cannot be. In config and module files, replace:
enum Foo {}with aconstobjectnamespace/moduleblocks with plain exports- constructor parameter properties (
constructor(private x: string) {}) with an explicit assignment - experimental decorators
TypeScript reports all of these for you as TS1294.
3. If you publish a layer or module, ship compiled JavaScript.
This one is specific to packages. The runtime refuses to strip types from any file inside node_modules, whatever the configuration, so a published entrypoint written in TypeScript cannot be loaded natively however new the Node version is. Build to JavaScript before publishing, and if your package ships a nuxt.config, emit it as nuxt.config.mjs. Otherwise every consuming project has to install jiti.
This does not apply to layers inside your own project: layers/*/nuxt.config.ts loads natively.
4. Install jiti if you still need it.
jiti is now an optional peer dependency. Install it and Nuxt will pick it up automatically as a fallback whenever the runtime cannot load a file on its own:
npm i -D jiti
yarn add -D jiti
pnpm add -D jiti
bun add -D jiti
A nuxt.schema file always needs jiti, whatever Node version you are on: its JSDoc annotations are read by an import-time transform rather than by importing the file.
One smaller change comes with this: PostCSS plugins named in postcss.plugins are now resolved by the runtime, so a plugin name that only resolves through a Nuxt alias entry no longer loads. Use the package name, or a path.
Migration to Vite Environment API
๐ฆ Impact Level: Medium
What Changed
Nuxt 5 migrates to Vite 6's new Environment API, which formalizes the concept of environments and provides better control over configuration per environment.
Previously, Nuxt used separate client and server Vite configurations. Now, Nuxt uses a shared Vite configuration with environment-specific plugins that use the applyToEnvironment() method to target specific environments.
experimental.viteEnvironmentApi option has been removed.Key changes:
- Deprecated environment-specific
extendViteConfig(): Theserverandclientoptions inextendViteConfig()are deprecated and will show warnings when used. - Changed plugin registration: Vite plugins registered with
addVitePlugin()and only targeting one environment (by passingserver: falseorclient: false) will not have theirconfigorconfigResolvedhooks called. - Shared configuration: The
vite:extendConfigandvite:configResolvedhooks now work with a shared configuration rather than separate client/server configs.
Reasons for Change
The Vite Environment API provides:
- Better consistency between development and production builds
- More granular control over environment-specific configuration
- Improved performance and plugin architecture
- Support for custom environments beyond just client and server
Migration Steps
1. Migrate to use Vite plugins
We would recommend you use a Vite plugin instead of extendViteConfig, vite:configResolved and vite:extendConfig.
// Before
extendViteConfig((config) => {
config.optimizeDeps.include.push('my-package')
}, { server: false })
nuxt.hook('vite:extendConfig' /* or vite:configResolved */, (config, { isClient }) => {
if (isClient) {
config.optimizeDeps.include.push('my-package')
}
})
// After
addVitePlugin(() => ({
name: 'my-plugin',
config (config) {
// you can set global vite configuration here
},
configResolved (config) {
// you can access the fully resolved vite configuration here
},
configEnvironment (name, config) {
// you can set environment-specific vite configuration here
if (name === 'client') {
config.optimizeDeps ||= {}
config.optimizeDeps.include ||= []
config.optimizeDeps.include.push('my-package')
}
},
applyToEnvironment (environment) {
return environment.name === 'client'
},
}))
2. Migrate Vite plugins to use environments
Instead of using addVitePlugin with server: false or client: false, you can instead use the new applyToEnvironment hook within your plugin.
// Before
addVitePlugin(() => ({
name: 'my-plugin',
config (config) {
config.optimizeDeps.include.push('my-package')
},
}), { client: false })
// After
addVitePlugin(() => ({
name: 'my-plugin',
config (config) {
// you can set global vite configuration here
},
configResolved (config) {
// you can access the fully resolved vite configuration here
},
configEnvironment (name, config) {
// you can set environment-specific vite configuration here
if (name === 'client') {
config.optimizeDeps ||= {}
config.optimizeDeps.include ||= []
config.optimizeDeps.include.push('my-package')
}
},
applyToEnvironment (environment) {
return environment.name === 'client'
},
}))
Migration to Vite 8
๐ฆ Impact Level: Medium
What Changed
Nuxt 5 upgrades from Vite 7 to Vite 8, which replaces esbuild and Rollup with Rolldown as the underlying bundler. This brings significantly faster builds but includes several breaking changes.
future.compatibilityVersion: 5. If you want to test Vite 8 compatibility ahead of time, you can add a "vite": "^8.0.0-beta.15" resolution override in your package.json.Most of the migration is handled by Nuxt internally, but there are some user-facing changes to be aware of:
vite.esbuildandvite.optimizeDeps.esbuildOptionsare deprecated in favour ofvite.oxcandvite.optimizeDeps.rolldownOptions. Vite 8 converts these automatically for now, but they will be removed in the future.build.rollupOptionsis deprecated in favour ofbuild.rolldownOptions.- CommonJS interop behaviour has changed. If you import CJS modules, review the Vite 8 migration guide for details.
Migration to Nitro v3
๐ฆ Impact Level: Significant
What Changed
Nuxt 5 upgrades to Nitro v3, which is a major rewrite of the server engine. Nitro v3 is built on srvx and h3 v2, adopting Web standard Request/Response APIs throughout. This brings performance improvements and a more consistent API, but includes several breaking changes to server-side code.
The sections below highlight changes that are most relevant to Nuxt application developers and module authors.
Package and Import Path Changes
The nitropack package has been renamed to nitro. All import paths have changed:
| Before | After |
|---|---|
nitropack | nitro |
nitropack/types | nitro/types |
nitropack/runtime | nitro |
h3 (for server utilities) | nitro/h3 |
Auto-imports within server routes (defineEventHandler, getQuery, readBody, useRuntimeConfig, etc.) continue to work without changes.
If you have explicit imports in server code, update them:
- import { defineEventHandler, getQuery } from 'h3'
+ import { defineEventHandler, getQuery } from 'nitro/h3'
For module authors, type augmentations must target the new module path:
- declare module 'nitropack/types' {
+ declare module 'nitro/types' {
interface NitroRouteRules {
myModule?: { /* ... */ }
}
}
Error Handling: status/statusText replace statusCode/statusMessage
h3 v2 renames the error properties to align with Web standards:
createError({
- statusCode: 404,
- statusMessage: 'Not Found',
+ status: 404,
+ statusText: 'Not Found',
})
In server routes, the error class is now HTTPError (replacing createError from h3):
- import { createError } from 'h3'
+ import { HTTPError } from 'nitro/h3'
export default defineEventHandler(() => {
- throw createError({ statusCode: 400, statusMessage: 'Bad request' })
+ throw new HTTPError({ status: 400, statusText: 'Bad request' })
})
app/ directory), Nuxt's createError composable continues to work and is the recommended way to throw errors.createError No Longer Returns an HTTPError Instance
NuxtError is now its own class rather than a subclass of h3's HTTPError, so instanceof HTTPError no longer matches an error created with Nuxt's createError. Everything else is unchanged: the error keeps the same properties, serialises the same way, and is still mapped to the right HTTP response during SSR.
If you narrow errors by class, use one of the predicates instead:
- if (error instanceof HTTPError) {
+ if (HTTPError.isError(error)) {
// handles both `new HTTPError()` and Nuxt's `createError()`
}
+ import { isNuxtError } from '#app'
+
+ if (isNuxtError(error)) {
+ // narrows to errors created with Nuxt's `createError`
+ }
Server Event API Changes (h3 v2)
The H3Event object now uses Web standard APIs:
Request properties:
- event.path // string
+ event.url.pathname // URL object - use .pathname, .search, .hash
- event.method // string
+ event.req.method // via Web Request object
- event.node.req.headers // Node.js IncomingHttpHeaders
+ event.req.headers // Web Headers API (.get(), .set(), .has())
Response properties:
- event.node.res.statusCode = 200
+ event.res.status = 200
- event.node.res.statusMessage = 'OK'
+ event.res.statusText = 'OK'
- setResponseHeader(event, 'x-custom', 'value')
+ event.res.headers.set('x-custom', 'value')
- appendResponseHeader(event, 'set-cookie', cookie)
+ event.res.headers.append('set-cookie', cookie)
useRuntimeConfig() No Longer Accepts event
In Nitro v3, useRuntimeConfig() no longer requires (or accepts) an event argument in server routes:
export default defineEventHandler((event) => {
- const config = useRuntimeConfig(event)
+ const config = useRuntimeConfig()
})
Route Rules: statusCode Renamed to status
If you define redirect route rules, the property name has changed:
export default defineNuxtConfig({
routeRules: {
'/old-page': {
- redirect: { to: '/new-page', statusCode: 302 },
+ redirect: { to: '/new-page', status: 302 },
},
},
})
For Module Authors: Additional Changes
- Nitro plugin imports: Use
import { definePlugin } from 'nitro'for explicit imports (auto-imports still work). - Runtime hooks:
nitroApp.hooks.hook('beforeResponse', ...)andnitroApp.hooks.hook('afterResponse', ...)have been replaced bynitroApp.hooks.hook('response', ...). getRouteRules()fromnitro/app: On the server, the Nitro helper changed fromgetRouteRules(event)togetRouteRules(method, pathname), which returns{ routeRules }.
Removal of experimental.externalVue
๐ฆ Impact Level: Minimal
What Changed
The experimental.externalVue option has been removed. Vue compiler dependencies (@babel/parser, @vue/compiler-core, @vue/compiler-dom, @vue/compiler-ssr, estree-walker) are now always replaced with mock proxies in the server bundle when vue.runtimeCompiler is not enabled.
Reasons for Change
With the migration to Nitro v3, all dependencies are bundled into the server output by default (unlike Nitro v2, which externalized node_modules). The externalVue option was originally designed to keep Vue as an external dependency, which was needed to avoid multiple copies of Vue from being bundled, but since Nitro v3 bundles everything regardless, the option became a no-op.
Vue's server builds include the full compiler toolchain, pulling @babel/parser (465KB) and other compiler packages into the server bundle unnecessarily. These compiler packages are only needed when vue.runtimeCompiler is enabled for runtime template compilation.
By always mocking these compiler dependencies, the default server bundle size is reduced by approximately 860KB (~59%).
Migration Steps
If you previously set experimental.externalVue explicitly, you should now remove it.
export default defineNuxtConfig({
experimental: {
- externalVue: false,
},
})
vue.runtimeCompiler: true, the real compiler packages are still included as before.experimental.parseErrorData Is No Longer Configurable
๐ฆ Impact Level: Minimal
What Changed
The experimental.parseErrorData option is deprecated, and on Nuxt 5 it is forced on. Setting it to false logs a warning and is otherwise ignored, so error.data on the error page is always the value you passed to createError.
On compatibilityVersion: 4 the option still works and false still gives you a stringified error.data, so the behaviour only changes when you opt in to Nuxt 5.
Reasons for Change
When Nuxt renders an error page it fetches /__nuxt_error internally, passing the error along with the request. That used to spread the error across one query parameter per field, and because query values are always strings, error.data arrived as a string and had to be parsed back. parseErrorData existed to opt out of that parsing.
The error is now sent as a single JSON-encoded parameter, so error.data keeps its original shape and is never stringified. Values also keep their types, so status is a number and booleans are booleans rather than the strings 'true' and 'false'. With nothing stringifying error.data, the option only survives to re-stringify it for apps that still expect a string.
Migration Steps
Remove the option:
export default defineNuxtConfig({
experimental: {
- parseErrorData: false,
},
})
If you set it to false in order to keep parsing error.data yourself, drop that parsing too, as error.data now keeps whatever value you passed to createError:
<script setup lang="ts">
import type { NuxtError } from '#app'
const props = defineProps({
error: Object as () => NuxtError
})
- const data = JSON.parse(props.error.data)
+ const data = props.error.data
</script>
@vitejs/plugin-vue-jsx Is Now Optional
๐ฆ Impact Level: Minimal
What Changed
@vitejs/plugin-vue-jsx is no longer installed by default with @nuxt/vite-builder. It is now an optional peer dependency that is loaded on demand only when a .jsx or .tsx file is encountered during the build.
If your project uses JSX/TSX components, Nuxt will automatically detect this and prompt you to install the package.
Reasons for Change
The @vitejs/plugin-vue-jsx plugin pulls in a significant dependency tree (Babel, @vue/babel-plugin-jsx, etc.) that is unnecessary for projects that don't use JSX. Making it optional reduces the default install size and speeds up dependency resolution for the majority of Nuxt projects.
Migration Steps
If your project uses .jsx or .tsx files, add @vitejs/plugin-vue-jsx as a dev dependency:
npm install -D @vitejs/plugin-vue-jsx
yarn add -D @vitejs/plugin-vue-jsx
pnpm add -D @vitejs/plugin-vue-jsx
bun add -D @vitejs/plugin-vue-jsx
Alternatively, Nuxt will prompt you to install it automatically the first time a JSX/TSX file is processed during development.
If your project does not use JSX, no changes are needed.
giget Is Now Optional (Remote Layers)
๐ฆ Impact Level: Minimal
What Changed
giget is no longer installed by default. It is now an optional peer dependency of @nuxt/kit, needed only to download a layer that extends names by remote URL:
export default defineNuxtConfig({
extends: ['github:my-org/my-theme'],
})
Local layers, layers in ~~/layers/, and layers installed as packages are unaffected.
Reasons for Change
Most projects never extend from a remote source, so the downloader was being installed for everyone to serve a small minority. Downloading a layer at config-resolution time is also the least reproducible way to consume one: the fetch happens outside your package manager, so it is not in your lockfile and, unless you pin it yourself, not pinned to a revision.
Migration Steps
Preferred: move the layer into package.json. Every major package manager understands git URLs, so a remote layer can be a normal dependency. This puts it in your lockfile, pins it to an exact commit, and installs it alongside everything else:
{
"devDependencies": {
"my-theme": "github:my-org/my-theme#4a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b"
}
}
Then extend from it by package name:
export default defineNuxtConfig({
extends: ['my-theme'],
})
The part after # is any commit-ish, so a tag (#v1.2.0) or #semver:^1.2.0 works too; a full commit SHA is the only form that is genuinely immutable.
Otherwise, install giget and keep using a remote extends entry:
npm install -D giget
yarn add -D giget
pnpm add -D giget
bun add -D giget
If a remote layer is resolved without giget present, Nuxt reports which extends entry needs it.
Removal of Legacy _renderResponse Support
๐ฆ Impact Level: Minimal
What Changed
ssrContext._renderResponse is no longer checked as a fallback. Only the internal ssrContext['~renderResponse'] (set by Nuxt's own router composable) is used.
Reasons for Change
The _renderResponse property on ssrContext was kept as a backward-compatibility fallback after #33896 migrated the internal API to ~renderResponse. The TODO comments indicated it should be removed in Nuxt v5.
Migration Steps
If you were setting ssrContext._renderResponse directly (which was never a public API), use ssrContext['~renderResponse'] instead. The Nuxt router composable already uses the new property, so no changes are needed if you're going through navigateTo or route middleware.
Non-Async callHook
๐ฆ Impact Level: Minimal
What Changed
With the upgrade to hookable v6, callHook may now return void instead of always returning Promise<void>. This is a significant performance improvement that avoids unnecessary Promise allocations when there are no registered hooks or all hooks are synchronous.
By default (with compatibilityVersion: 4), Nuxt wraps callHook with Promise.resolve() so that existing .then() and .catch() chaining continues to work. With compatibilityVersion: 5, this wrapper is removed.
Reasons for Change
Hookable v6's callHook is 20-40x faster because it avoids creating a Promise when one is not needed. This benefits applications with many hook call sites.
Migration Steps
If you or your modules use callHook with .then() or .catch() chaining, switch to await:
- nuxtApp.callHook('my:hook', data).then(() => { ... })
+ await nuxtApp.callHook('my:hook', data)
- nuxtApp.hooks.callHook('my:hook', data).catch(err => { ... })
+ try { await nuxtApp.hooks.callHook('my:hook', data) } catch (err) { ... }
future.compatibilityVersion: 5 (see Testing Nuxt 5) or by enabling it explicitly with experimental.asyncCallHook: false.Alternatively, you can ensure callHook always returns a Promise with:
export default defineNuxtConfig({
experimental: {
asyncCallHook: true,
},
})
Client-Only Comment Placeholders
๐ฆ Impact Level: Minimal
What Changed
With compatibilityVersion: 5, client-only components (.client.vue files and createClientOnly() wrappers) now render an HTML comment (<!--placeholder-->) on the server instead of an empty <div> element.
Reasons for Change
When the placeholder <div> and the actual component root share the same tag name, Vue's runtime skips re-applying setScopeId during hydration. This causes scoped styles to be missing after the component mounts. Using a comment node avoids the tag name collision entirely.
Migration Steps
If you rely on the placeholder <div> to inherit attributes (class, style, etc.) for layout purposes (e.g., reserving space to prevent layout shift), wrap the component in <ClientOnly> with a #fallback slot instead:
- <MyComponent class="placeholder" style="min-height: 200px" />
+ <ClientOnly>
+ <MyComponent />
+ <template #fallback>
+ <div class="placeholder" style="min-height: 200px"></div>
+ </template>
+ </ClientOnly>
future.compatibilityVersion: 5 (see Testing Nuxt 5) or by enabling it explicitly with experimental.clientNodePlaceholder: true.Alternatively, you can revert to the previous <div> placeholder behavior with:
export default defineNuxtConfig({
experimental: {
clientNodePlaceholder: false,
},
})
Stricter Side-Effect Imports
๐ฆ Impact Level: Minimal
What Changed
With compatibilityVersion: 5, Nuxt's generated tsconfig.json enables noUncheckedSideEffectImports. This is a default in TypeScript 7, so adopting it early keeps your project aligned ahead of that upgrade.
With this option on, a side-effect-only import (import './setup') that TypeScript cannot resolve to a module is now a type error, whereas it was previously ignored. This only affects type-checking (nuxt typecheck and your editor), not runtime behavior.
Reasons for Change
Unresolved side-effect imports were silently ignored, so a typo or a deleted file could pass type-checking. Flagging them catches these mistakes and matches the TypeScript 7 default.
Migration Steps
If type-checking now errors on a side-effect import of a non-code asset (for example import '~/assets/styles.css'), add an ambient module declaration so TypeScript knows the import is valid:
declare module '*.css' {}
nuxt.config:export default defineNuxtConfig({
typescript: {
tsConfig: {
compilerOptions: {
noUncheckedSideEffectImports: false,
},
},
},
})
Vue Options API Disabled by Default
๐ฆ Impact Level: Minimal
What Changed
With compatibilityVersion: 5, Nuxt sets Vue's __VUE_OPTIONS_API__ feature flag to false, which compiles Vue's Options API runtime out of the client bundle.
Reasons for Change
The Options API runtime ships in every client bundle even though most Nuxt applications are written with the Composition API and <script setup>. Dropping it shrinks the client bundle (around 6 kB minified / 2 kB gzipped on a minimal app).
Migration Steps
If any of your components (or a dependency's components) use the Options API (export default { data() {}, methods: {}, ... }), re-enable it in your nuxt.config:
export default defineNuxtConfig({
vue: {
optionsApi: true,
},
})
defineNuxtComponent is unaffected: its asyncData and head options are handled through setup() rather than the Vue Options API, so it works regardless of this flag.Typed Pages Enabled by Default
๐ฆ Impact Level: Minimal
What Changed
With compatibilityVersion: 5, experimental.typedPages is enabled by default. Nuxt generates typed route names and paths from your pages/ directory, so composables like useRoute, navigateTo, <NuxtLink> and router.push are type-checked against your actual routes.
Reasons for Change
Typed routing catches broken links and stale route names at type-check time rather than at runtime, and it is now native to vue-router, so it is a sensible default.
Migration Steps
If you reference routes that don't exist (for example a typo in a to prop, or a route defined dynamically outside the pages/ directory), type-checking will now error. Fix the reference, or extend the generated route types for routes you add at runtime.
future.compatibilityVersion: 5 (see Testing Nuxt 5) or by enabling it explicitly with experimental.typedPages: true.Alternatively, you can opt out and revert to the previous behavior with:
export default defineNuxtConfig({
experimental: {
typedPages: false,
},
})
Typed $fetch Rebuilt on Generated Route Types
๐ฆ Impact Level: Medium
What Changed
$fetch and useFetch are typed from the routes Nuxt's server builder reports, rather than from
nitro's InternalApi interface. As well as response types, a body, query or headers the
matching handler validates are now enforced on the call, and a method the route does not handle is
rejected.
Two things follow from this:
- Routes you augmented by hand on nitro's
InternalApiare no longer picked up. AugmentServerRoutesfrom@nuxt/schemainstead. - The
paramsoption is no longer accepted by$fetchoruseFetch. It was an alias forquery.
Reasons for Change
Nitro v3 dropped typed fetch, and the old implementation did not scale past about 300 routes. The new
generated route types cost the same to check whatever the route count, and carry enough information to type the
request as well as the response. params is dropped so the name is free for named route parameters
later.
Migration Steps
Move any params option to query:
const { data } = await useFetch('/api/search', {
- params: { q: 'nuxt' },
+ query: { q: 'nuxt' },
})
Move any manual route augmentation from InternalApi to ServerRoutes:
- declare module 'nitropack' {
- interface InternalApi {
- '/api/hello': { get: { message: string } }
- }
- }
+ import type { Endpoint } from 'nuxt/app'
+
+ declare module '@nuxt/schema' {
+ interface ServerRoutes {
+ '/api/hello': {
+ [Endpoint]: {
+ GET: { response: { message: string } }
+ }
+ }
+ }
+ }
TypeScript baseUrl Is Ignored
๐ฆ Impact Level: Minimal
What Changed
With compatibilityVersion: 5, Nuxt removes compilerOptions.baseUrl from its generated TypeScript configurations. Relative Nuxt and Nitro aliases are resolved from Nuxt's build directory instead of a custom baseUrl.
Reasons for Change
TypeScript 6 deprecates baseUrl, and it is no longer required when using paths. Removing the option also keeps Nuxt's generated aliases anchored consistently to the generated configuration that contains them.
Migration Steps
Remove baseUrl from your Nuxt TypeScript configuration. If you used it to anchor a relative Nuxt or Nitro alias, make that alias absolute instead:
+ import { fileURLToPath } from 'node:url'
+
export default defineNuxtConfig({
alias: {
- images: './assets/images',
+ images: fileURLToPath(new URL('./assets/images', import.meta.url)),
},
- typescript: {
- tsConfig: {
- compilerOptions: {
- baseUrl: '..',
- },
- },
- },
})