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()
}
})
Navigation Failure Handling
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.
Related Articles
For complete routing implementation, check out how to use Vue Router and how to implement authentication in Vue.



