skip to content

In Nuxt 4, should a recipe site's auth check be app/middleware/auth.global.ts or a named auth middleware on account.vue, and what ordering and double-run effects follow?

level: seniorimportance: should knowfreq 38%

answer

  1. opt-in page versus every route
  2. parent meta covers the children
  3. globals run alphabetically, first
  4. server render, then again in browser
  5. exclude /login or it loops

basics

~20 s

For one protected /account area, a named app/middleware/auth.ts set on account.vue with definePageMeta covers every /account child and skips public recipe pages. A .global file runs on every navigation, alphabetically before page middleware, and must exempt /login.

solid answer

~40 s

Nuxt 4 has three kinds of route middleware: inline functions in `definePageMeta`, named files in `app/middleware/` referenced by name, and `.global` files that run on every route change. For one protected area I put `middleware: 'auth'` on the parent `account.vue`: Nuxt collects middleware from every matched route, so `/account/orders` is covered, and public `/recipes` pages never pay for the check. A global `auth.global.ts` suits a deny-by-default app, but it must let `/login` and public paths through or it loops. Globals run first, alphabetically by filename (hence `01.` prefixes), then page middleware in array order. On a server-rendered first load every middleware runs twice, in the server render and again in the browser, so the session must be readable on both sides: a cookie, not `localStorage`.

code

vue · 11 lines
vue
<!-- app/pages/account.vue -->
<script setup lang="ts">
// applies to /account and every child, e.g. /account/orders
definePageMeta({ middleware: 'auth' })
</script>

<template>
  <section>
    <NuxtPage />
  </section>
</template>

go deeper

for a junior

Recall the three kinds: inline in definePageMeta, named files in app/middleware/, and .global files that run on every route change.

for a middle

Explain the order: globals alphabetically by filename, then page middleware in array order, and why a parent route's middleware also covers its child routes.

for a senior

Choose scope on purpose: opt-in named middleware or deny-by-default global, loop-proof exclusions, cookie sessions for the double run, and server-side checks for APIs.

for a principal

Frame opt-in versus deny-by-default as a policy choice: which failure you prefer when someone adds a new page without thinking about authentication.

## Three kinds of route middleware **Route middleware** in Nuxt 4 is code that runs during page navigation, inside the Vue part of the app. It is distinct from Nitro **server middleware**, which runs for HTTP requests on the server. There are three ways to declare it: | Kind | Where | Runs for | Referenced by | |---|---|---|---| | Inline | a function in `definePageMeta({ middleware })` | that page and its child routes | nothing; it is the function | | Named | `app/middleware/auth.ts` | pages that list it, and their children | `'auth'` in `definePageMeta` | | Global | `app/middleware/auth.global.ts` | every route change | nothing; the `.global` suffix registers it | Named middleware is loaded through a dynamic import when first needed. Names are normalised to kebab-case (`myMiddleware` becomes `my-middleware`), the generated `MiddlewareKey` type makes a misspelled name a type error, and at runtime an unknown name throws `NUXT_E2004`. ## Scoping the check to /account For a recipe site whose only private area is `/account`, a named middleware on the parent page is the tight fit: 1. `app/middleware/auth.ts` holds the check and returns `navigateTo('/login')` when there is no session. 2. `app/pages/account.vue`, the parent of `account/index.vue`, `account/orders.vue` and the rest, declares `definePageMeta({ middleware: 'auth' })`. 3. On navigation, Nuxt builds the middleware list from **every record in `to.matched`**, so `/account/orders` picks up the parent's `auth` even though `orders.vue` never mentions it. 4. Entries are collected in a `Set`, so a child that repeats `'auth'` still runs it once. Public pages such as `/recipes/lemon-tart` never load or run the check. ## When a global middleware is the right call A `.global` middleware inverts the default into **deny by default**. That is safer when most of the app is private and new pages appear often, because a page someone forgets to annotate is still protected. It costs: - a run on **every** navigation, public pages included; - an explicit allow-list with `/login` at minimum, or the redirect to `/login` triggers the middleware again and loops; Vue Router's loop detection and warning exist in development builds only; - upkeep of that allow-list as public routes are added. For a mostly public recipe site with one private area, opt-in named middleware is simpler; for a mostly private app, global deny-by-default is the sturdier policy. Calling `addRouteMiddleware(name, fn, { global: true })` from a plugin is the programmatic form of a `.global` file. ## Order of execution For one navigation, Nuxt runs middleware one after another and stops at the first that redirects or aborts: 1. Nuxt's own `validate` middleware, which runs the page's `validate` from `definePageMeta`. 2. Global middleware files, **alphabetically by filename**: `analytics.global.ts` before `setup.global.ts`. Prefix `01.`, `02.` to force an order, remembering that names sort as strings, so `10.` comes before `2.`. 3. Global middleware added with `addRouteMiddleware`. 4. Page-declared middleware from the matched routes, in array order. So a global middleware that prepares the session must sort before any middleware that reads it. ## Server and client: the double run On a server-rendered or generated site, middleware for the **first page** runs twice: once during the server render and again in the browser for the initial route. Later navigations run in the browser only. Consequences: - the session source must exist on both sides, such as a cookie read with `useCookie`, never `localStorage`; - side effects such as analytics events fire twice unless guarded; - you can skip one side with `import.meta.server` or `import.meta.client`, or skip only the hydration run by checking `isHydrating` and `payload.serverRendered` on `useNuxtApp()`. For auth, keep the server run: skipping it sends protected HTML before the browser redirects. ## Registration details and boundaries - Nuxt 4 scans the top-level files in `app/middleware/` and, new in Nuxt 4, an `index` file inside each subfolder; other nested files are not registered. - Route rules can add or remove app middleware for path patterns through `appMiddleware`. - Route middleware **never runs for Nitro server routes** such as `/api/*`; those need server middleware or a check in the handler. - Inside middleware, read `to` and `from`; `useRoute()` does not describe the route being checked, and Nuxt warns about it there.

  • How do you stop a route middleware from running twice on the first server-rendered load, and should an auth check do that?
    Return early with `import.meta.server` or `import.meta.client` to skip a side, or skip only the hydration run with `import.meta.client && nuxtApp.isHydrating && nuxtApp.payload.serverRendered`, where `nuxtApp = useNuxtApp()`. For auth I keep both runs as long as the check is a cheap cookie read; skipping the server run would send protected HTML to a signed-out visitor.
  • Why should the middleware read to rather than calling useRoute()?
    Middleware runs before the navigation is confirmed, so there is no current route yet; `useRoute()` does not describe the route being checked, and Nuxt warns when it is called inside middleware. The `to` argument is the target, so the path, params and `to.fullPath` for a `next` query come from it, and helpers used by middleware should take the route as a parameter.
  • Does this route middleware protect /api/account/orders?
    No. Route middleware runs in the Vue part of the app on page navigations and does not run for Nitro server routes such as `/api/*`. Those endpoints need their own session check, in server middleware or in the handler, or the data stays reachable by anyone who calls the API directly.

saying these in an interview costs you the question

  • Page-declared middleware runs before global middleware
  • Middleware on account.vue does not apply to /account/orders
  • Global middleware files run in the order they were created
  • Route middleware runs once, in the browser, after hydration
  • Route middleware also guards /api server routes
  • localStorage is a fine session source for route middleware on SSR