Skip to content
Release notesNuxt

Nuxt 4.3: appLayout in routeRules, the #server alias and ISR payloads

Nuxt 4.3 was released on 22 January, with Nuxt 3.21 beside it. Layouts can move into nuxt.config.ts, cached routes get payload files, and createError learns the Web API names.

vuejs.school team27 Jan 20268 min readNuxt 4.3

Nuxt 4.3 is a minor release, published on 22 January together with Nuxt 3.21. Most of its new APIs are opt-in: a layout chosen per URL pattern in nuxt.config.ts, route groups that middleware can check, a #server import alias and new names for the status fields of createError. Two changes reach your app as soon as you upgrade. Routes with isr, swr or cache rules start using payload files, and the server's TypeScript settings get stricter, so nuxt typecheck may report new errors in server/.

The examples use the Nuxt 4 folder layout: pages/, layouts/ and middleware/ live inside app/, and server/ stays at the project root.

What shipped

The release post lists these changes:

  • A new appLayout property in routeRules, so a layout can be set for a whole path pattern instead of in each page's definePageMeta.
  • Payload extraction for routes with isr, swr and cache rules, which used to work only for prerendered pages, and payload extraction in development.
  • Route groups, the folders wrapped in parentheses such as (protected)/, now appear in route.meta.groups.
  • setPageLayout() accepts a second argument with props for the layout.
  • A #server alias for imports inside the server/ directory, with protection against importing it from client or shared code.
  • The development error overlay from Nuxt 4.2 can be dragged to a corner, minimised to a small pill, and keeps its position across reloads.
  • A module inherited from a layer can be disabled by passing false as its options, for example image: false.
  • statusCode and statusMessage on createError are deprecated in favour of status and statusText.
  • Module authors can pass async functions to addVitePlugin and addWebpackPlugin, and inlineStyles now works with webpack and rspack as well as Vite.

Under the hood, route rules are now compiled into a client chunk with rou3, which removes radix3 from the client bundle and the app manifest fetches that went with it. The plugin that handles SSR styles got faster, and internal hooks use filters so they run less often.

Set a layout for a whole path with appLayout

Until now a page chose its layout itself, with definePageMeta({ layout: 'admin' }) in its script block. For an admin area with thirty pages that is thirty identical lines, and a new page that forgets the line renders in the default layout. From 4.3 the layout can follow the URL instead:

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/admin/**': { appLayout: 'admin' },
    '/auth/**': { appLayout: 'minimal' },
  },
})

routeRules is where Nuxt already keeps per-path settings such as caching, redirects and headers. The keys are path patterns, and ** matches any depth. appLayout names a file in app/layouts/, so 'admin' means app/layouts/admin.vue. Every page under /admin/ now renders inside it, with no definePageMeta call.

appLayout only sets a default. A layout set in the page with definePageMeta({ layout }), or by a setPageLayout() call, takes priority, so a single page can still pick another one.

Layouts can also receive props. setPageLayout(), the composable that switches the layout at runtime, takes a second argument, and the layout declares the props as any component would:

app/middleware/admin.ts
export default defineNuxtRouteMiddleware(() => {
  setPageLayout('admin', { sidebar: true, theme: 'dark' })
})
app/layouts/admin.vue
<script setup lang="ts">
defineProps<{
  sidebar?: boolean
  theme?: 'light' | 'dark'
}>()
</script>

<template>
  <div :data-theme="theme">
    <aside v-if="sidebar">Admin menu</aside>
    <slot />
  </div>
</template>

Payload files for ISR, SWR and cached routes

When Nuxt renders a page on the server, the data that useFetch and useAsyncData loaded is serialised into the payload, a JSON blob sent with the HTML so the browser does not fetch it again. For prerendered pages Nuxt has long written that payload to a separate _payload.json file next to the HTML. A client-side navigation to such a page loads the small JSON file instead of calling your APIs. Until 4.3, that stopped at pages rendered at build time.

In 4.3 it also applies to pages rendered on request and then cached, the routes with isr, swr and cache rules:

  • ISR, incremental static regeneration, renders a page on its first request and serves the cached copy for a set time.
  • SWR, stale-while-revalidate, serves the cached copy at once and refreshes it in the background.
  • A cache rule stores the rendered page on the server with Nitro's cache options, such as { maxAge: 60 } in seconds.

All three now produce a _payload.json that a CDN such as Vercel, Netlify or Cloudflare can cache beside the HTML. A navigation from a listing to a product page then fetches one cached file rather than calling every API the page uses.

You do not switch this on: a route that already has one of these rules gets payload files after the upgrade. This rule, for example, caches the product pages with ISR and gives them the shop layout:

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/products/**': { isr: 3600, appLayout: 'shop' },
  },
})

The 3600 is seconds: a product page is regenerated at most once an hour.

Payload extraction now works in development too, when nitro.static is true or when a page has an isr, swr, prerender or cache rule. You can watch the _payload.json request appear in the network tab of your browser's developer tools without making a production build first.

Check route groups in middleware

Route groups are folders wrapped in parentheses, app/pages/(protected)/dashboard.vue for example. The parentheses keep the folder out of the URL, so that page is served at /dashboard, and the folder exists only to organise files. From 4.3 the group names are exposed in the route's meta as groups, which turns a folder into something middleware can check:

app/middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to) => {
  const session = useCookie('session')
  if (to.meta.groups?.includes('protected') && !session.value) {
    return navigateTo('/login')
  }
})

A middleware file ending in .global.ts runs on every navigation. Moving a page into the (protected) folder is now enough to put it behind the check, and no page needs a definePageMeta({ middleware: 'auth' }) line. The ?. is there because pages outside any group have no groups entry. The session cookie stands in for however your app knows that someone is signed in.

Import server code with #server

#shared already pointed at the shared/ folder. Now #server points at server/, so a handler deep in the tree imports a helper without counting dots:

server/api/products/[id].get.ts
import { findProduct } from '#server/utils/products'

export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  const product = await findProduct(id)
  if (!product) {
    throw createError({ status: 404, statusText: 'Product not found' })
  }
  return product
})

The alias comes with import protection. If a component, a composable or a file in shared/ imports from #server, Nuxt refuses the import, so server code such as a database client cannot reach the browser bundle through the alias.

Rename statusCode and statusMessage in createError

The handler above already uses the new error fields. Nitro v3 and h3 v2 are moving to the names the Web API uses: a Response has status and statusText, not statusCode and statusMessage. Nuxt 4.3 adopts them and deprecates the old names ahead of Nuxt 5. The old names still work and print no warning, so write new code with the new ones and rename old calls when you next touch them:

server/api/products/[id].get.ts
// Deprecated in 4.3
throw createError({ statusCode: 404, statusMessage: 'Product not found' })

// Current
throw createError({ status: 404, statusText: 'Product not found' })

In pages, components and middleware, createError is Nuxt's own, and its types mark the old names @deprecated. Your editor's autocomplete lists them as deprecated, but a call that already uses them is not flagged. In server/, createError comes from h3 1.15, which accepts both spellings and marks neither. The Nuxt docs now use the new names in both places. A search for statusCode: across server/ and app/ finds every old call.

Nuxt 3 support runs to 31 July 2026

When Nuxt 4 shipped, the end of life for Nuxt 3 was set for 31 January 2026. The 4.3 post moves it: security updates and critical bug fixes continue, and Nuxt 3 now reaches end of life on 31 July 2026. Nuxt 3.21, released the same day, backports the route rule layouts, the ISR payload extraction, the layout props, the #server alias, the draggable overlay, module disabling and the performance work. If you are still on 3.x, take 3.21 now and plan the move to 4 before July, following the upgrade guide.

The post also says the main branch will begin receiving commits for Nuxt 5 in the coming weeks, with changes backported to the 4.x and 3.x branches, and that the upgrade guide will describe how to opt in early with future.compatibilityVersion: 5. That is a plan, not a release.

How to upgrade and what to check

Upgrade now: the two changes that arrive on their own are quick to check, and the rest waits until you use it. One command updates Nuxt and deduplicates the lockfile, which pulls in updates to the packages Nuxt relies on:

npx nuxt upgrade --dedupe

For a project staying on Nuxt 3, the same command takes a channel:

npx nuxt@latest upgrade --dedupe --channel=v3

Then run npx nuxt typecheck. The server tsconfig now enables noUncheckedIndexedAccess, which types every array element and every dynamic object key as possibly undefined. A line such as const first = rows[0] followed by first.id now reports that first is possibly undefined. The fix is a guard or an early createError.

The release post recommends keeping the option on, because turning it off lets indexed reads that can fail at runtime slip through. It shows the opt-out for a codebase that needs time:

nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    typescript: {
      tsConfig: {
        compilerOptions: { noUncheckedIndexedAccess: false },
      },
    },
  },
})

The TypeScript config also enables allowArbitraryExtensions by default. It lets you import a file with an unusual extension when a declaration file named like styles.d.css.ts sits beside it, so it only adds possibilities.

Last, if you have isr, swr or cache rules, click through to one of those pages and check that its _payload.json request appears in the network tab.

Check it stuck3 questions from the Rendering modes and deployment quiz.
Practice now
Was this helpful?

Keep reading