Learn how to implement navigation guards in Nuxt 3 using defineNuxtRouteMiddleware. This guide covers the essentials of route protection, async logic handling, and middleware ordering.
Get notified when we release new tutorials, lessons, and other expert Nuxt content.
Navigation guards are an essential feature in any application that needs to handle access control, route redirection, or run logic before entering a page. In Nuxt 3, we can easily create these guards using defineNuxtRouteMiddleware
. This function allows you to intercept route changes and apply logic before completing the navigation process.
In this article, we’ll explore what navigation guards are, how they work in Nuxt 3, and dive into creating them using defineNuxtRouteMiddleware
. We’ll also look at some best practices and advanced techniques as we go.
In web development, navigation guards allow you to control access to certain routes. These guards are especially useful for:
In Nuxt 3, navigation guards are built with defineNuxtRouteMiddleware
, which makes it easy to hook into the routing system and define custom rules.
defineNuxtRouteMiddleware
?defineNuxtRouteMiddleware
is an auto-imported helper function in Nuxt 3 that lets you define route-specific middleware logic. Middleware in Nuxt 3 works similarly to Express.js, but it's focused on handling route transitions. The middleware can be defined inlined, globally, or per-page, giving you flexibility in controlling route behavior.
Before we dive into complex scenarios, let’s start with a simple navigation guard. In Nuxt 3, you can define route middleware inside a page or globally.
In any page, we can have an inlined middleware using the usePageMeta composable:
// pages/dashboard.vue
<script setup lang="ts">
const { user } = useAuth()
definePageMeta({
middleware: [
function (to, from) {
const isAdmin = user.value?.role
if (!isAdmin) {
// Redirect the user to the homepage
return navigateTo('/')
}
}
]
})
</script>
In this example, we added an inlined route middleware to the pages/dashboard.vue
page. This middleware checks if the logged in user is admin, and if not, it will redirect the user to the homepage.
Here’s how to add a navigation guard to a single page using defineNuxtRouteMiddleware
.
Inside the ./middleware/
directory, create a new file named adminOnlyGuard.ts
:
// middleware/adminOnlyGuard.ts
export default defineNuxtRouteMiddleware((to, from) => {
const isAdmin = false // Can be dynamic data coming from a utility function or a composable
// If user is not an admin
if (!isAdmin) {
// Redirect the user to the homepage
return navigateTo('/')
}
})
In this example, we are using the isAdmin
variable which hypothetically represents the current user role. If the user is not admin and they’re trying to visit a protected route, they are redirected to the homepage.
Then, in the dashboard
page, we can register this route middleware or navigation guard via definePageMeta
:
// pages/dashboard.vue
<script setup lang="ts">
definePageMeta({
middleware: ["admin-only-guard"]
})
</script>
This is how you can protect specific pages with custom route guards in Nuxt.
Sometimes, it’s more efficient to apply guards globally across the entire application, like checking for authentication on every route.
To create a global middleware, place it in the middleware/
folder and add a .global
suffix to the name:
// middleware/auth.global.js
export default defineNuxtRouteMiddleware((to, from) => {
const { user } = useAuthState()
const isLoggedIn = user.value
if (!isLoggedIn && to.path !== '/login') {
return navigateTo('/login');
}
});
In this example, we’re checking if the user is not logged in and is trying to visit any page other than the login page. If so, we redirect the user to the login page.
You don’t need to define this middleware in every page — Nuxt will automatically apply it globally. This is a powerful feature that helps keep your app organized and DRY.
Understanding the order in which middleware executes is crucial for ensuring your Nuxt 3 application behaves as expected. Middleware runs in a specific sequence that includes both global middleware and page-defined middleware. Here’s how the order works:
middleware/
directory with the .global
suffix.Example Directory Structure:
middleware/
│
├── track.global.ts
├── log.global.ts
├── auth.ts
└── validate.ts
Example Page Component:
<!-- pages/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
middleware: [
'auth',
'validate',
function (to, from) {
// Custom inline middleware
console.log("Navigating to:", to.path);
}
],
});
</script>
Execution Order:
Given the above structure and component, the middleware will run in the following order:
log.global.ts
track.global.ts
auth.ts
validate.ts
Notice how the log
middleware is executed first. Global middleware is executed in an alphabetical order.
By default, global middleware executes alphabetically based on their filenames. However, you might want to enforce a specific order, particularly if certain middleware depends on others.
To achieve this, you can prefix global middleware filenames with numerical values.
Example Directory with Custom Order:
middleware/
│
├── 01.initialize.global.ts
├── 02.fetch.global.ts
└── validate.js
Remember that filenames are sorted as strings. For instance, 10.fetch.global.ts
will be executed before 2.initialize.global.ts
. To maintain proper order, use zero-padding (e.g., 01
, 02
) for single-digit numbers.
In some cases, you might want to run asynchronous logic in your route middleware, such as fetching data from an API or validating a token. The good news is that defineNuxtRouteMiddleware
supports async/await.
export default defineNuxtRouteMiddleware(async (to, from) => {
const user = await getUserFromApi();
if (!user || !user.isAuthenticated) {
return navigateTo('/login');
}
});
In this example, we fetch the current user data asynchronously before deciding whether they should be allowed to proceed. This is useful for apps that need to validate tokens or fetch remote data before allowing access to a route.
Now that we’ve covered how to use defineNuxtRouteMiddleware
, let’s talk about some best practices to ensure your middleware logic remains clean, efficient, and easy to maintain.
Middleware should be kept simple and focused on one task, such as authentication or permission checking. If your middleware becomes too complex, consider breaking it down into smaller, reusable pieces.
To avoid duplicating logic across multiple middleware functions, use Vue 3 composables to share state and logic across your application.
// composables/useAuthState.js
import { ref } from 'vue';
export const useAuthState = () => {
const isLoggedIn = ref(false);
// Logic to check login state
return { isLoggedIn };
};
By using composables like useAuthState
, you ensure that your logic is reusable across different middleware and components.
Global middleware runs before every route change, so avoid placing heavy logic that could block the UI. Fetching data or running CPU-intensive tasks could slow down navigation.
Instead, fetch data only once and store it in a Pinia store when possible. This ensures that your app stays fast and responsive, even when handling complex logic.
Creating navigation guards in Nuxt 3 with defineNuxtRouteMiddleware
provides you with a powerful and flexible way to control route access. Whether you’re protecting routes, redirecting users, or running asynchronous logic before navigation, the ability to customize your app’s behavior at the routing level is a must-have feature for any serious web application.
For more detailed info, be sure to check out the Mastering Nuxt 3 course to dive even deeper!