---
title: "Route middleware in Nuxt 4: three kinds, one job · vuejs.school"
description: "Anonymous, named and global route middleware in Nuxt 4, the order they run, what to return, where they run, and why a server route still has to check the cookie."
canonical_url: "https://vuejs.school/community/blog/route-middleware-in-nuxt-4"
meta:
  "og:description": "Anonymous, named and global route middleware in Nuxt 4, the order they run, what to return, where they run, and why a server route still has to check the cookie."
  "og:image": "https://vuejs.school/_og/d/a_~Um91dGUgbWlkZGxld2FyZSBpbiBOdXh0IDQ6IHRocmVlIGtpbmRzLCBvbmUgam9iIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~Um91dGUgbWlkZGxld2FyZSBpbiBOdXh0IDQ6IHRocmVlIGtpbmRzLCBvbmUgam9i%2Cdescription_~QW5vbnltb3VzLCBuYW1lZCBhbmQgZ2xvYmFsIHJvdXRlIG1pZGRsZXdhcmUgaW4gTnV4dCA0LCB0aGUgb3JkZXIgdGhleSBydW4sIHdoYXQgdG8gcmV0dXJuLCB3aGVyZSB0aGV5IHJ1biwgYW5kIHdoeSBhIHNlcnZlciByb3V0ZSBzdGlsbCBoYXMgdG8gY2hlY2sgdGhlIGNvb2tpZS4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9yb3V0ZS1taWRkbGV3YXJlLWluLW51eHQtNCI%2Cs_rbjFhCcSvLIRue1j.png"
  "og:image:alt": "Route middleware in Nuxt 4: three kinds, one job · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Route middleware in Nuxt 4: three kinds, one job · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~Um91dGUgbWlkZGxld2FyZSBpbiBOdXh0IDQ6IHRocmVlIGtpbmRzLCBvbmUgam9iIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~Um91dGUgbWlkZGxld2FyZSBpbiBOdXh0IDQ6IHRocmVlIGtpbmRzLCBvbmUgam9i%2Cdescription_~QW5vbnltb3VzLCBuYW1lZCBhbmQgZ2xvYmFsIHJvdXRlIG1pZGRsZXdhcmUgaW4gTnV4dCA0LCB0aGUgb3JkZXIgdGhleSBydW4sIHdoYXQgdG8gcmV0dXJuLCB3aGVyZSB0aGV5IHJ1biwgYW5kIHdoeSBhIHNlcnZlciByb3V0ZSBzdGlsbCBoYXMgdG8gY2hlY2sgdGhlIGNvb2tpZS4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9yb3V0ZS1taWRkbGV3YXJlLWluLW51eHQtNCI%2Cs_rbjFhCcSvLIRue1j.png"
  "twitter:image:alt": "Route middleware in Nuxt 4: three kinds, one job · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## What route middleware is

A route middleware is a function Nuxt calls with two arguments: `to`, the route the visitor is going to, and `from`, the route they are leaving. It runs in the Vue part of the app, on the server during the first render and in the browser on every client-side navigation. It is not the same thing as server middleware in `server/middleware/`, which runs inside Nitro for every HTTP request and never sees a Vue route.

A Vue Router guard can take a third argument, a `next()` callback. Nuxt middleware gets none. You return a value, or nothing to let the navigation carry on.

## Anonymous middleware for one page

The quickest kind lives inside the page that needs it, in `definePageMeta()`. It suits a rule that only one page cares about.

*app/pages/checkout.vue*

```
<script setup lang="ts">
definePageMeta({
  middleware: (to) => {
    if (!to.query.cart) {
      return navigateTo('/cart')
    }
  },
})
</script>

<template>
  <h1>Checkout</h1>
</template>
```

Here a `cart` query parameter stands in for the shop's real cart state.

`definePageMeta` is a compiler macro, so Nuxt extracts it at build time and knows the page's middleware before the component's code is downloaded. The price is that the macro cannot use the component's own values, such as a `ref` declared in `<script setup>`. It can use imports and pure functions defined in the same file.

## Named middleware shared by several pages

Put a file in `app/middleware/` and its name becomes the middleware's name. `auth.ts` is `auth`; a file called `requireAdmin.ts` would be normalised to `require-admin`. The file is loaded only when a page lists it.

*app/middleware/auth.ts*

```
export default defineNuxtRouteMiddleware((to) => {
  const session = useCookie('session')

  if (!session.value) {
    return navigateTo({
      path: '/login',
      query: { redirect: to.fullPath },
    })
  }
})
```

`useCookie()` works inside middleware because middleware runs with the Nuxt app context. On the server it reads the cookie from the incoming request; in the browser it reads `document.cookie`. Passing the original path as a `redirect` query lets the login page send the visitor back afterwards.

A page opts in by name:

*app/pages/account.vue*

```
<script setup lang="ts">
definePageMeta({
  middleware: ['auth'],
})
</script>

<template>
  <h1>Your account</h1>
</template>
```

## Global middleware for every route

Add `.global` before the extension and the file runs on every route change, with no page having to list it. Use it for things that are true of the whole site.

*app/middleware/01.maintenance.global.ts*

```
export default defineNuxtRouteMiddleware((to) => {
  const config = useRuntimeConfig()

  if (config.public.maintenance && to.path !== '/maintenance') {
    return navigateTo('/maintenance')
  }
})
```

This reads a public runtime config value, so it needs `runtimeConfig: { public: { maintenance: false } }` in `nuxt.config.ts`, and you flip it at deploy time with the environment variable `NUXT_PUBLIC_MAINTENANCE=true`. Without the `to.path` check the redirect would loop, because arriving at `/maintenance` is also a route change.

## The order middleware runs in

Global middleware runs first, then the page's list in the order written. Global files run alphabetically by file name, and file names sort as strings, so `10.x.global.ts` comes before `2.x.global.ts`. The numeric prefix in `01.maintenance.global.ts` is how you pin the order: pad with a zero and leave gaps for files you add later.

For a page with `middleware: ['auth', 'admin']`, the full sequence is every `.global.ts` file in name order, then `auth`, then `admin`. The first one that returns a redirect or an abort stops the chain, so put the cheap checks first. There is no point running an admin check for a visitor who is not even signed in.

## Redirect with navigateTo, stop with abortNavigation

Nuxt has two helpers for this, and the docs recommend them over the other return values Vue Router accepts.

`navigateTo()` redirects. It takes a path or a route object, and two of its options matter in middleware:

- `redirectCode` sets the status of the redirect the server sends for a first request: 302 by default, 301 for a permanent move.
- `external: true` allows a target on another site. Without it, Nuxt refuses an external URL with an error.

`abortNavigation()` stops the navigation. In the browser the visitor stays on the current page. On the first, server-rendered request there is no such page, so Nuxt shows a 404 error page.

Pass it an error to show the error page with that error. On the server that always works, but in a client-side navigation Nuxt shows the page only when the error has `fatal: true`. Without that flag the visitor stays where they are, with nothing on screen to say why. This middleware, for a page such as `app/pages/posts/[slug].vue`, sets the flag:

*app/middleware/valid-slug.ts*

```
export default defineNuxtRouteMiddleware((to) => {
  const slug = String(to.params.slug ?? '')

  if (!/^[a-z0-9-]+$/.test(slug)) {
    return abortNavigation(
      createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true }),
    )
  }
})
```

## Middleware on the server and in the browser

For a server-rendered page, middleware for the first request runs twice: once on the server while rendering, and once more in the browser while Nuxt hydrates, the step where Vue takes over the server's HTML. After that, every client-side navigation runs it once, in the browser only.

Most middleware does not care. When it does, Nuxt gives you three checks. `import.meta.server` is `true` only in the server build and `import.meta.client` only in the browser build, and the bundler removes the dead branch. To skip only the repeat during hydration, check the app's state, as this version of the `auth` middleware does:

*app/middleware/auth.ts*

```
export default defineNuxtRouteMiddleware((to) => {
  const nuxtApp = useNuxtApp()

  // The server already ran this check for the current request
  if (import.meta.client && nuxtApp.isHydrating && nuxtApp.payload.serverRendered) {
    return
  }

  const session = useCookie('session')
  if (!session.value) {
    return navigateTo({ path: '/login', query: { redirect: to.fullPath } })
  }
})
```

The `serverRendered` part matters where `ssr: false` turns server rendering off: there the browser's run is the only one, so it must not be skipped.

The browser run also changes what `useCookie()` can see. A session cookie should be `httpOnly`, which means the browser never shows it to JavaScript. Then `useCookie('session')` sees it on the server but reads `undefined` in the browser, and a client-side navigation to `/account` would bounce a signed-in visitor.

A simple answer is a second, readable cookie that only says "signed in", set and cleared alongside the real one. Point the middleware's `useCookie()` at that marker instead of `session`. The server never trusts the marker; it checks the real cookie.

## Protect data in the server route, not the middleware

Route middleware decides what the visitor sees. After the first page it runs in a browser the visitor controls, and it only runs when Nuxt navigates. Anyone can open the browser's developer tools and call `fetch('/api/orders')` directly, and no middleware runs for that request. If the server route hands over the data, the data is public.

*server/api/orders.get.ts**Breaks***

```
export default defineEventHandler(() => {
  // The auth middleware already checked the cookie, so this must be safe
  return [{ id: 41, total: 32 }, { id: 42, total: 18 }]
})
```

Every server route that returns something private has to check the cookie itself, and so does every route that changes data.

*server/api/orders.get.ts**Works***

```
export default defineEventHandler((event) => {
  const session = getCookie(event, 'session')

  if (!session) {
    throw createError({ statusCode: 401, statusMessage: 'Sign in first' })
  }

  // Look up the orders that belong to this session; a fixed list stands in here
  return [{ id: 41, total: 32 }, { id: 42, total: 18 }]
})
```

Here `getCookie()` reads the real session cookie from the request, httpOnly or not, because the server sees every header. The lookup from session to user belongs in a server utility you call from each protected route, and the cookie should hold a signed or sealed value, never a plain user id.

With that in place, the route middleware becomes what it should be: a courtesy that saves a signed-out visitor a round trip to a page they could not use anyway.

**Check it stuck**3 questions from the Middleware, sessions and auth quiz.

[**Practice now**](https://vuejs.school/learn/nuxt/middleware-sessions-and-auth?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Release notes · 7 min read**What's new in Nuxt 4.6: nuxt/server, typed $fetch, cookie sessions and Vapor**](https://vuejs.school/community/blog/whats-new-in-nuxt-4-6) [Tutorial · 6 min read**useFetch or $fetch? Picking the right one in Nuxt**](https://vuejs.school/community/blog/usefetch-or-fetch-picking-the-right-one-in-nuxt)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/route-middleware-in-nuxt-4#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-08-21","description":"Anonymous, named and global route middleware in Nuxt 4, the order they run, what to return, where they run, and why a server route still has to check the cookie.","name":"Route middleware in Nuxt 4: three kinds, one job","url":"https://vuejs.school/community/blog/route-middleware-in-nuxt-4","breadcrumb":{"@id":"https://vuejs.school/community/blog/route-middleware-in-nuxt-4#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/route-middleware-in-nuxt-4"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/route-middleware-in-nuxt-4#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-08-21","description":"Anonymous, named and global route middleware in Nuxt 4, the order they run, what to return, where they run, and why a server route still has to check the cookie.","headline":"Route middleware in Nuxt 4: three kinds, one job","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~Um91dGUgbWlkZGxld2FyZSBpbiBOdXh0IDQ6IHRocmVlIGtpbmRzLCBvbmUgam9iIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~Um91dGUgbWlkZGxld2FyZSBpbiBOdXh0IDQ6IHRocmVlIGtpbmRzLCBvbmUgam9i%2Cdescription_~QW5vbnltb3VzLCBuYW1lZCBhbmQgZ2xvYmFsIHJvdXRlIG1pZGRsZXdhcmUgaW4gTnV4dCA0LCB0aGUgb3JkZXIgdGhleSBydW4sIHdoYXQgdG8gcmV0dXJuLCB3aGVyZSB0aGV5IHJ1biwgYW5kIHdoeSBhIHNlcnZlciByb3V0ZSBzdGlsbCBoYXMgdG8gY2hlY2sgdGhlIGNvb2tpZS4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9yb3V0ZS1taWRkbGV3YXJlLWluLW51eHQtNCI%2Cs_rbjFhCcSvLIRue1j.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/route-middleware-in-nuxt-4#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/route-middleware-in-nuxt-4#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/route-middleware-in-nuxt-4#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Route middleware in Nuxt 4: three kinds, one job","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~Um91dGUgbWlkZGxld2FyZSBpbiBOdXh0IDQ6IHRocmVlIGtpbmRzLCBvbmUgam9iIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~Um91dGUgbWlkZGxld2FyZSBpbiBOdXh0IDQ6IHRocmVlIGtpbmRzLCBvbmUgam9i%2Cdescription_~QW5vbnltb3VzLCBuYW1lZCBhbmQgZ2xvYmFsIHJvdXRlIG1pZGRsZXdhcmUgaW4gTnV4dCA0LCB0aGUgb3JkZXIgdGhleSBydW4sIHdoYXQgdG8gcmV0dXJuLCB3aGVyZSB0aGV5IHJ1biwgYW5kIHdoeSBhIHNlcnZlciByb3V0ZSBzdGlsbCBoYXMgdG8gY2hlY2sgdGhlIGNvb2tpZS4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9yb3V0ZS1taWRkbGV3YXJlLWluLW51eHQtNCI%2Cs_rbjFhCcSvLIRue1j.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~Um91dGUgbWlkZGxld2FyZSBpbiBOdXh0IDQ6IHRocmVlIGtpbmRzLCBvbmUgam9iIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~Um91dGUgbWlkZGxld2FyZSBpbiBOdXh0IDQ6IHRocmVlIGtpbmRzLCBvbmUgam9i%2Cdescription_~QW5vbnltb3VzLCBuYW1lZCBhbmQgZ2xvYmFsIHJvdXRlIG1pZGRsZXdhcmUgaW4gTnV4dCA0LCB0aGUgb3JkZXIgdGhleSBydW4sIHdoYXQgdG8gcmV0dXJuLCB3aGVyZSB0aGV5IHJ1biwgYW5kIHdoeSBhIHNlcnZlciByb3V0ZSBzdGlsbCBoYXMgdG8gY2hlY2sgdGhlIGNvb2tpZS4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9yb3V0ZS1taWRkbGV3YXJlLWluLW51eHQtNCI%2Cs_rbjFhCcSvLIRue1j.png"}]}
```