How to use Vue Router guards

Vue Router guards enable control over navigation flow with hooks that run before, during, and after route transitions. As the creator of CoreUI with 12 years of Vue development experience, I’ve implemented router guards in production Vue applications that protect authenticated routes and manage complex navigation logic for millions of users.

The most secure approach combines global guards for authentication with per-route guards for role-based access control.

Global Before Guards

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/login', component: Login },
    { path: '/dashboard', component: Dashboard, meta: { requiresAuth: true } },
    { path: '/admin', component: Admin, meta: { requiresAuth: true, role: 'admin' } }
  ]
})

router.beforeEach((to, from, next) => {
  const isAuthenticated = localStorage.getItem('authToken')
  const userRole = localStorage.getItem('userRole')

  if (to.meta.requiresAuth && !isAuthenticated) {
    next('/login')
  } else if (to.meta.role && userRole !== to.meta.role) {
    next('/')
  } else {
    next()
  }
})

export default router

Per-Route Guards

const routes = [
  {
    path: '/profile',
    component: Profile,
    beforeEnter: (to, from, next) => {
      if (isUserLoggedIn()) {
        next()
      } else {
        next('/login')
      }
    }
  },
  {
    path: '/admin',
    component: Admin,
    beforeEnter: (to, from, next) => {
      if (hasAdminAccess()) {
        next()
      } else {
        next({ name: 'Forbidden', params: { message: 'Admin access required' } })
      }
    }
  }
]

Component Guards

<script>
export default {
  name: 'EditPost',

  beforeRouteEnter(to, from, next) {
    // Called before route is confirmed
    // Does NOT have access to `this`
    fetchPost(to.params.id).then(post => {
      next(vm => {
        vm.post = post
      })
    })
  },

  beforeRouteUpdate(to, from, next) {
    // Called when route changes but component is reused
    // Has access to `this`
    this.post = null
    fetchPost(to.params.id).then(post => {
      this.post = post
      next()
    })
  },

  beforeRouteLeave(to, from, next) {
    // Called when navigating away
    if (this.hasUnsavedChanges) {
      const answer = window.confirm('You have unsaved changes. Leave anyway?')
      next(answer)
    } else {
      next()
    }
  }
}
</script>

Async Authentication Guard

import { useAuthStore } from '@/stores/auth'

router.beforeEach(async (to, from, next) => {
  const authStore = useAuthStore()

  if (to.meta.requiresAuth) {
    if (!authStore.isAuthenticated) {
      try {
        await authStore.checkAuth()
        next()
      } catch (error) {
        next({
          path: '/login',
          query: { redirect: to.fullPath }
        })
      }
    } else {
      next()
    }
  } else {
    next()
  }
})

Role-Based Access Control

const routes = [
  {
    path: '/admin',
    component: AdminLayout,
    meta: { requiresAuth: true, roles: ['admin', 'superadmin'] },
    children: [
      {
        path: 'users',
        component: UserManagement,
        meta: { roles: ['admin', 'superadmin'] }
      },
      {
        path: 'settings',
        component: Settings,
        meta: { roles: ['superadmin'] }
      }
    ]
  }
]

router.beforeEach((to, from, next) => {
  const userRole = localStorage.getItem('userRole')
  const requiredRoles = to.meta.roles

  if (requiredRoles && !requiredRoles.includes(userRole)) {
    next({ name: 'Forbidden' })
  } else {
    next()
  }
})

Global After Hooks

router.afterEach((to, from) => {
  // Update page title
  document.title = to.meta.title || 'My App'

  // Track page view
  if (window.gtag) {
    window.gtag('config', 'GA_MEASUREMENT_ID', {
      page_path: to.fullPath
    })
  }

  // Scroll to top
  window.scrollTo(0, 0)
})

Loading State Guard

import { ref } from 'vue'

export const isLoading = ref(false)

router.beforeEach((to, from, next) => {
  isLoading.value = true
  next()
})

router.afterEach(() => {
  isLoading.value = false
})

// Usage in App.vue
// <div v-if="isLoading" class="loading">Loading...</div>

Permission-Based Guards

import { useUserStore } from '@/stores/user'

function hasPermission(permission) {
  const userStore = useUserStore()
  return userStore.permissions.includes(permission)
}

const routes = [
  {
    path: '/posts/create',
    component: CreatePost,
    beforeEnter: (to, from, next) => {
      if (hasPermission('posts.create')) {
        next()
      } else {
        next({ name: 'Forbidden' })
      }
    }
  },
  {
    path: '/posts/:id/edit',
    component: EditPost,
    beforeEnter: (to, from, next) => {
      if (hasPermission('posts.edit')) {
        next()
      } else {
        next({ name: 'Forbidden' })
      }
    }
  }
]

Redirect After Login

// Login component
import { useRouter, useRoute } from 'vue-router'

export default {
  setup() {
    const router = useRouter()
    const route = useRoute()

    const login = async (credentials) => {
      try {
        await authService.login(credentials)

        const redirect = route.query.redirect || '/dashboard'
        router.push(redirect)
      } catch (error) {
        console.error('Login failed:', error)
      }
    }

    return { login }
  }
}

Data Fetching Guard

const routes = [
  {
    path: '/users/:id',
    component: UserProfile,
    beforeEnter: async (to, from, next) => {
      try {
        const user = await fetchUser(to.params.id)

        if (user) {
          to.params.user = user
          next()
        } else {
          next({ name: 'NotFound' })
        }
      } catch (error) {
        next({ name: 'Error', params: { error } })
      }
    }
  }
]

Composition API Guards

<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'

const hasUnsavedChanges = ref(false)

onBeforeRouteLeave((to, from, next) => {
  if (hasUnsavedChanges.value) {
    const answer = window.confirm('Discard unsaved changes?')
    next(answer)
  } else {
    next()
  }
})

onBeforeRouteUpdate(async (to, from, next) => {
  if (to.params.id !== from.params.id) {
    await loadData(to.params.id)
  }
  next()
})
</script>

Guard with Pinia Store

// stores/auth.js
import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null,
    token: localStorage.getItem('token')
  }),

  getters: {
    isAuthenticated: (state) => !!state.token,
    isAdmin: (state) => state.user?.role === 'admin'
  },

  actions: {
    async checkAuth() {
      if (this.token) {
        const response = await fetch('/api/me', {
          headers: { Authorization: `Bearer ${this.token}` }
        })
        this.user = await response.json()
      }
    }
  }
})

// router/index.js
import { useAuthStore } from '@/stores/auth'

router.beforeEach(async (to, from, next) => {
  const authStore = useAuthStore()

  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    next('/login')
  } else if (to.meta.requiresAdmin && !authStore.isAdmin) {
    next('/')
  } else {
    next()
  }
})
import { NavigationFailureType, isNavigationFailure } from 'vue-router'

router.push('/dashboard').catch(failure => {
  if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
    console.log('Navigation aborted')
  } else if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
    console.log('Navigation duplicated')
  }
})

Advanced Guard Composition

function createAuthGuard(options = {}) {
  return async (to, from, next) => {
    const { loginPath = '/login', checkAuth = true } = options

    if (!to.meta.requiresAuth) {
      return next()
    }

    const authStore = useAuthStore()

    if (checkAuth && !authStore.isAuthenticated) {
      return next({
        path: loginPath,
        query: { redirect: to.fullPath }
      })
    }

    if (to.meta.roles && !to.meta.roles.includes(authStore.user?.role)) {
      return next({ name: 'Forbidden' })
    }

    next()
  }
}

router.beforeEach(createAuthGuard())

Best Practice Note

This is the same navigation guard architecture we use in CoreUI’s Vue admin templates. Guards provide centralized control over route access and navigation flow. Always handle async operations properly in guards, use route meta fields for configuration, and implement proper error handling for failed navigations. Store sensitive auth checks server-side and use guards only for UI/UX improvements.

For production applications, consider using CoreUI’s Vue Admin Template which includes pre-configured router guards with authentication and role-based access control.

For complete routing implementation, check out how to use Vue Router and how to implement authentication in Vue.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.
How to change opacity on hover in CSS
How to change opacity on hover in CSS

How to Disable Right Click on a Website Using JavaScript
How to Disable Right Click on a Website Using JavaScript

What is globalThis in JavaScript?
What is globalThis in JavaScript?

How to Remove Underline from Link in CSS
How to Remove Underline from Link in CSS

Answers by CoreUI Core Team