How to create Navigation Guards in Nuxt with defineNuxtRouteMiddleware

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.

Mostafa Said
Nuxt 3

The Mastering Nuxt FullStack Unleashed Course is here!

Get notified when we release new tutorials, lessons, and other expert Nuxt content.

Click here to view course

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.

What Are Navigation Guards?

In web development, navigation guards allow you to control access to certain routes. These guards are especially useful for:

  • Protecting routes that require authentication.
  • Redirecting users based on their roles.
  • Running specific logic before navigating to a page (like fetching data).

In Nuxt 3, navigation guards are built with defineNuxtRouteMiddleware, which makes it easy to hook into the routing system and define custom rules.

What is 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.

Setting Up Navigation Guards in Nuxt 3

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.

1. Inlined Route Middleware

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.

2. Named Route Middleware

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.

3. Global Route Middleware

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.

Middleware Ordering

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:

  1. Global Middleware
    Global middleware files are executed first. These are typically placed in the middleware/ directory with the .global suffix.
  2. Page-Specific Middleware
    If a page defines multiple middleware using the array syntax, those are executed next.

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:

  1. log.global.ts
  2. track.global.ts
  3. auth.ts
  4. validate.ts
  5. Custom inline middleware

Notice how the log middleware is executed first. Global middleware is executed in an alphabetical order.

Customizing Global Middleware 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.

Advanced Usage: Asynchronous Guards

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.

Best Practices for Route Middleware

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.

1. Keep Middleware Simple

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.

2. Use Composables for Shared Logic

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.

3. Avoid Blocking Navigation with Heavy Logic

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.

Conclusion

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!

Mostafa Said
Mostafa is a full-stack developer, a full-time Instructor at Vue School, and a Vue.js Jedi.

Follow MasteringNuxt on