Nuxt Route Guards
While traditional Vue applications rely heavily on navigation hooks like beforeEach and beforeEnter from Vue Router, Nuxt (specifically Nuxt 3) takes a more streamlined approach by using route middleware to handle route guarding. These middleware functions run right before a page component is rendered, offering seamless Server-Side Rendering (SSR) compatibility. They allow you to authenticate users, redirect requests, or halt navigation entirely before the route transition finishes.
To create a reusable middleware, place your file inside the middleware/ directory at the root of your project, and then bind it to specific pages using definePageMeta:
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const token = useCookie('token')
if (!token.value) {
return navigateTo('/login')
}
})
<!-- pages/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
middleware: ['auth']
})
</script>
2. Global Middleware
If you append .global to your middleware filename, Nuxt will automatically apply it to every route transition. This is ideal for cross-cutting concerns like analytics tracking or global permission setup:
// middleware/logger.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
console.log(`Navigating from ${from.fullPath} to ${to.fullPath}`)
})
3. Inline Middleware
For lightweight checks that only apply to a single component, you can define the middleware directly inside the page's definePageMeta:
<script setup lang="ts">
definePageMeta({
middleware: [
function (to, from) {
if (to.params.id === '0') {
return navigateTo('/products')
}
}
]
})
</script>
Controlling Navigation via Return Values
The true power of route middleware lies in what you return, as it dictates the final outcome of the navigation process:
- Returning nothing: Allows the navigation to proceed normally.
return navigateTo('/login'): Redirects the user to a different internal route.return navigateTo('https://example.com', { external: true }): Redirects to an external URL.return abortNavigation(): Cancels the navigation and throws an error.return abortNavigation(createError({ statusCode: 403, statusMessage: 'Forbidden' })): Halts navigation and returns a customized error object.
The navigateTo utility also accepts options like replace: true (which replaces the current history entry instead of pushing a new one) and redirectCode (specifying the HTTP status code for server-side redirects, defaulting to 302), covering almost every redirection scenario you might encounter.
Practical Example: Authentication Flow
Let us combine useCookie to build a common real-world requirement: redirecting unauthenticated users to a login page while preserving their original destination so they can be redirected back after logging in.
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const token = useCookie('token')
// Prevent logged-in users from visiting the login page
if (to.path === '/login' && token.value) {
return navigateTo('/')
}
// Redirect unauthenticated users to login and attach the target path
if (!token.value) {
return navigateTo(`/login?redirect=${encodeURIComponent(to.fullPath)}`)
}
})
After a successful login, you can read the redirect query parameter to complete the journey back:
<script setup lang="ts">
async function handleLogin() {
await api.login(form)
const redirect = useRoute().query.redirect as string
await navigateTo(redirect || '/', { replace: true })
}
Because middleware natively supports async/await, you can easily fetch permissions from an API or integrate with user state modules managed by useState.
Compatibility with Native Vue Router Hooks
If you are migrating an older codebase or require hooks like afterEach that middleware cannot fulfill, you can access the underlying Vue Router instance via a plugin:
// plugins/router-guard.ts
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.$router.afterEach((to, from) => {
// Example: Track page views
})
})
Note that using beforeEach and manually manipulating next() within plugins can easily cause conflicts with the SSR lifecycle. Any logic involving redirects and authentication should always rely on route middleware instead.
Execution Order and Best Practices
- Global middleware executes first, ordered alphabetically by filename, followed by page-level middleware.
- Page-level middleware runs in the exact order they are declared within the
middlewarearray, allowing you to mix named middleware and inline functions seamlessly. - Middleware runs on both the server and the client. Avoid direct references to browser-only globals like
windowordocument, and useimport.meta.clientwhen environment checks are necessary. - Route parameter changes on the same page (such as navigating from
/user/1to/user/2) will also trigger the middleware, making it a great place to handle data refreshing. - If your sole objective is to validate whether route parameters are valid, using
definePageMeta({ validate })is lighter and more appropriate than a full middleware function.
Summary
Nuxt provides a unified, SSR-friendly approach to route guarding through its middleware architecture. Named middleware handles localized interception, global middleware manages cross-cutting concerns, and inline middleware deals with page-specific details. Combined with navigateTo and abortNavigation, you gain precise control over every single navigation event to meet virtually all modern web application authorization and routing requirements.