Suggest an editImprove this articleRefine the answer for “What is middleware in Nuxt and how to use it?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Nuxt middleware** is a function that runs before a page mounts, used for auth checks, redirects, and access control. Types: global (every route, add `.global.ts`), named (opt-in via `definePageMeta`), inline (one-off function in `definePageMeta`). Returns `navigateTo()` to redirect or `abortNavigation()` to stop. **Key point:** runs before the component mounts, so it can block rendering entirely.Shown above the full answer for quick recall.Answer (EN)Image**Nuxt middleware** is a function that runs before a page component mounts, letting you check conditions, redirect users, or block navigation entirely. ## Theory ### TL;DR - Think of it like airport security: checks your ticket (route), lets you through, sends you to check-in (login), or stops you cold - Three types: global (every route), named (opt-in per page), inline (one-off inside `definePageMeta`) - Returns `navigateTo()` to redirect or `abortNavigation()` to stop without changing the URL - Use it for auth and route guards; skip it for data fetching (that is what `useAsyncData` is for) - Global middleware runs in alphabetical order by filename, before named, before inline ### Quick example ```ts // middleware/auth.global.ts export default defineNuxtRouteMiddleware((to, from) => { const token = useCookie('auth').value; if (!token && to.path !== '/login') { return navigateTo('/login'); // redirect unauthenticated users } // no return = proceed normally }); ``` Any unauthenticated user hitting `/dashboard` gets sent to `/login`. Authenticated users pass through. The `.global.ts` suffix means it runs on every route automatically, no extra configuration needed. ### Key difference from Vue composables Middleware intercepts navigation at the router level, before the page component mounts. A `useFetch` or `onMounted` call runs after. That timing gap is what lets middleware block rendering entirely and prevent flashes of private content. If you put an auth check in `onMounted`, the page renders first, then redirects. I have watched teams debug "flashing dashboard" issues for days before tracing it back to this exact timing difference. Bad UX, worse security. ### Types of middleware **Global middleware** lives in `middleware/` with the `.global.ts` suffix. Nuxt picks it up automatically and runs it on every route change. Alphabetical order among globals determines sequence. **Named middleware** also lives in `middleware/` but without the suffix. You attach it to specific pages via `definePageMeta`: ```ts // pages/admin/users.vue definePageMeta({ middleware: 'admin' // runs middleware/admin.ts }); ``` **Inline middleware** is a function defined directly inside `definePageMeta`. Good for one-off checks that do not need reuse: ```ts definePageMeta({ middleware: (to, from) => { if (!hasFeatureFlag('beta')) { return navigateTo('/'); } } }); ``` ### When to use - Auth checks and session validation: global or named middleware with `useCookie()` or a Pinia store - Role-based access (admin-only pages): named middleware attached via `definePageMeta` - One-time feature flags or experiment gates: inline middleware - SEO redirects (canonical URLs, legacy paths): global middleware Skip middleware for data loading. `useAsyncData` and `useFetch` handle that. Skip it for UI logic too. That belongs in `onMounted` or composables. ### How it works internally When a route change fires, Nuxt collects all middleware functions in order: globals first (alphabetical by filename), then named from the page meta, then inline. They run one by one. The first function that returns something other than `null` or `undefined` stops the chain. On the server (SSR), Nitro runs the same middleware before `render:page`. On the client, Vue Router's `beforeEach` hook handles it. So on initial load, middleware runs twice: once on the server, once when the client hydrates. That is why `localStorage` does not work in middleware. It does not exist on the server. ### Common mistakes **1. Mutating `to.path` directly** ```ts // wrong to.path = '/login'; // does nothing ``` `to` is a readonly `RouteLocation` object. Vue Router ignores direct mutations. Use `return navigateTo('/login')` instead. **2. Using `localStorage` for auth checks** ```ts // wrong const token = localStorage.getItem('token'); // undefined on server ``` SSR has no `localStorage`. The middleware runs on the server, finds nothing, and redirects. Then on the client it finds the token and passes. Inconsistent behavior, hydration mismatch. Use `useCookie()` or `useState()`. Both work on server and client. **3. Losing redirect history** ```ts // wrong - user cannot return after login return navigateTo('/login'); // right return navigateTo({ path: '/login', query: { redirect: from.path } }); ``` Pass `from.path` as a query parameter. After login, read it and send the user where they were going. **4. Global middleware blocking Nuxt DevTools** A global auth check also intercepts internal Nuxt paths like `/__nuxt_devtools__`. Add an early return: ```ts if (to.path.startsWith('/__')) return; // skip internal paths ``` **5. Async without await** ```ts // wrong - chain continues before fetch resolves fetch('/api/user').then(user => { /* ... */ }); // right const user = await $fetch('/api/user'); if (!user) return navigateTo('/login'); ``` Without `await`, the middleware returns `undefined` immediately and the next function in the chain fires before your async check finishes. ### Real-world usage - `nuxt-auth` module: wraps session check in `auth.global.ts` using the `useAuth()` composable - A/B testing: named middleware assigns the user to a variant and redirects to the matching page - Stripe checkout: inline middleware checks session before showing the payment page - Legacy URL redirects: global middleware maps old paths to new ones before any page renders ### Follow-up questions **Q:** What is the difference between `navigateTo()` and `abortNavigation()`? **A:** `navigateTo()` changes the URL and renders a different page. `abortNavigation()` stops the current navigation without changing the URL. You can pass an error object to `abortNavigation()` to render Nuxt's error page with a custom message. **Q:** In what order does middleware run? **A:** Global middleware runs first, in alphabetical order by filename. Then named middleware in the order listed in `definePageMeta`. Then inline. The chain stops at the first non-null return value. **Q:** Why does my middleware run twice on initial load? **A:** With SSR, middleware runs once on the server and once on the client during hydration. On subsequent client-side navigations it only runs in the browser. Use `process.server` or `process.client` guards if you need to limit execution to one environment. **Q:** Can middleware access data fetched by `useAsyncData`? **A:** No. Middleware runs before `useAsyncData` resolves. For shared data, use `useState()` or a Pinia store populated in a server plugin or earlier in the request lifecycle. **Q:** How would you build cache-aware middleware that skips repeated auth checks on recent visits? **A:** Store a short-lived cookie after successful validation: `useCookie('lastCheck', { maxAge: 300 })`. On the next request, if that cookie exists and the session cookie is also present, skip the API call and return. Both cookies sync between server and client, so SSR stays consistent. This is the pattern to reach for on high-traffic routes where hitting the auth API on every page load is too expensive. ## Examples ### Basic: global auth guard ```ts // middleware/auth.global.ts export default defineNuxtRouteMiddleware((to, from) => { const token = useCookie('auth').value; if (!token && to.path !== '/login') { // preserve the intended destination return navigateTo({ path: '/login', query: { redirect: to.path } }); } }); ``` Every protected page redirects unauthenticated users to `/login?redirect=/dashboard`. After login, read `route.query.redirect` and send them where they were going. Without the `query` part, users land on the home page every time. ### Intermediate: role-based access in a SaaS dashboard ```ts // middleware/admin.ts export default defineNuxtRouteMiddleware((to) => { const { user } = useUserStore(); // Pinia store if (!user.loggedIn) { return navigateTo('/login'); } if (user.role !== 'admin' && to.path.startsWith('/admin')) { abortNavigation('Admin access required'); // renders error page } }); // pages/admin/users.vue definePageMeta({ middleware: 'admin' }); ``` An admin gets the page. A logged-in non-admin sees the error page. A guest goes to `/login`. Three outcomes, one middleware file, zero duplicated checks across admin pages. ### Advanced: chaining global middleware with async operations ```ts // middleware/auth.global.ts (runs first - 'a' before 'l' alphabetically) export default defineNuxtRouteMiddleware((to) => { const token = useCookie('token').value; if (!token) { // 401 error page renders, URL does not change, no content flash throw createError({ statusCode: 401, message: 'Unauthorized' }); } }); // middleware/logger.global.ts (runs only if auth passed) export default defineNuxtRouteMiddleware(async (to) => { await $fetch('/api/log', { method: 'POST', body: { path: to.path } }); }); ``` Visiting a protected route without a token: auth throws 401, logger never runs. With a valid token: auth passes, logger records the visit. The `await` in logger delays rendering until the log request completes. If that is a performance concern, remove `await` and accept the trade-off that some log entries may be missed on fast navigations.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.