Suggest an editImprove this articleRefine the answer for “Vue router: routing and navigation guards”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Vue Router** maps URL paths to Vue components and controls navigation using guards that run before, during, or after route changes. ```typescript router.beforeEach((to) => { if (to.meta.requiresAuth && !auth.isLoggedIn) { return { path: '/login', query: { redirect: to.fullPath } } } }) ``` **Key:** Guards run in order: `beforeEach` → `beforeEnter` → `beforeResolve` → `afterEach`. In Vue Router 4, return a value from the guard instead of calling `next()`.Shown above the full answer for quick recall.Answer (EN)Image**Vue Router** maps URL paths to Vue components and controls navigation flow with guards - code that runs before, during, or after a route change. ## Theory ### TL;DR - Vue Router = hotel front desk: URLs are room numbers, components are rooms, guards check ID at the door before letting anyone in - Guard execution order: `beforeEach` (global) → `beforeEnter` (per-route) → async components load → `beforeResolve` (global) → navigation commits → `afterEach` - Use `beforeEach` for auth redirects, `beforeResolve` for data prefetching, `afterEach` for analytics - Always put `/:pathMatch(.*)*` last in your route array, or it swallows every path before specific routes get a chance to match ### Quick example ```typescript // router/index.ts import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: () => import('./Home.vue') }, { path: '/dashboard', component: () => import('./Dashboard.vue'), meta: { requiresAuth: true } // read in the guard below }, { path: '/:pathMatch(.*)*', component: () => import('./NotFound.vue') } // wildcard last ] }) router.beforeEach((to) => { // Runs before every navigation if (to.meta.requiresAuth && !localStorage.getItem('token')) { return { path: '/login', query: { redirect: to.fullPath } } } }) export default router ``` Visiting `/dashboard` without a token redirects to `/login?redirect=/dashboard`. With a token in `localStorage`, the Dashboard component loads normally. ### Guards vs plain routing Without guards, Vue Router is passive: URL changes, component swaps in, done. Guards turn that into active flow control. They pause the navigation, run your code, and only proceed if you allow it. The difference matters when your app has auth, permissions, or data that must load before a page renders. Basic routing has no way to stop that. Guards do. ### Three types of guards **Global guards** attach to the router instance and run for every navigation in the app: ```typescript router.beforeEach((to) => { const auth = useAuthStore() if (to.meta.requiresAuth && !auth.isLoggedIn) { return '/login' } }) router.afterEach((to) => { analytics.track(to.fullPath) // fires after every successful navigation }) ``` **Per-route guards** go directly in the route config. Useful when the logic applies to one route and does not need to pollute the global guard: ```typescript const routes = [ { path: '/admin', component: () => import('@/views/Admin.vue'), beforeEnter: async (to, from) => { const user = await fetchUser() if (!user.isAdmin) return '/unauthorized' } } ] ``` **In-component guards** use composables inside a component. The main use case is blocking navigation away from an unsaved form: ```vue <script setup> import { onBeforeRouteLeave } from 'vue-router' onBeforeRouteLeave((to, from) => { if (hasUnsavedChanges.value) { return window.confirm('Leave without saving?') } }) </script> ``` ### Guard execution order This is the sequence Vue Router follows on every navigation: 1. `router.beforeEach` (global) 2. `beforeEnter` on the target route 3. Async component resolution (dynamic imports run here) 4. `router.beforeResolve` (global, after components load) 5. Navigation commits, URL changes 6. `router.afterEach` (global, post-navigation) `beforeEach` runs before any component loads, so it is cheap for redirects. `beforeResolve` is better for data fetches because by that point the component is confirmed to load. I have seen teams put heavy API calls in `beforeEach` and wonder why components flash before redirecting - that is why. ### Route meta fields Meta fields let you attach data to any route and read it inside guards: ```typescript const routes = [ { path: '/profile/:id', component: UserProfile, meta: { requiresAuth: true, role: 'user' } } ] router.beforeEach((to) => { if (to.meta.requiresAuth && !store.auth.isLoggedIn) { return '/login?redirect=' + to.fullPath } }) ``` `to.meta` is available inside any guard, including global ones. Meta fields on parent routes are also accessible on child routes via `to.matched`. ### Common mistakes **Calling `next()` twice.** The callback-style API with `next` is still supported but trips developers regularly. Not returning after calling `next` means both branches execute: ```typescript // Wrong: both run when condition is false router.beforeEach((to, from, next) => { if (!auth) next('/login') // falls through! next() }) // Fix: early return router.beforeEach((to, from, next) => { if (!auth) return next('/login') next() }) // Better: use Vue Router 4 return style, drop next entirely router.beforeEach((to) => { if (!auth) return '/login' }) ``` **Wildcard route in the wrong position.** `/:pathMatch(.*)*` matches any path. Put it first and nothing else in your route array gets evaluated: ```typescript // Wrong: /users/:id never matches routes: [ { path: '/:pathMatch(.*)*', component: NotFound }, // catches everything { path: '/users/:id', component: UserProfile } ] // Fix: wildcard last routes: [ { path: '/users/:id', component: UserProfile }, { path: '/:pathMatch(.*)*', component: NotFound } ] ``` **`afterEach` logs stale page views.** Calling `next(false)` cancels navigation but `afterEach` still fires. Your analytics logs a page view for a page the user never reached. Check `to.fullPath !== from.fullPath` inside `afterEach`, or set a flag when cancelling. **Data fetching in `beforeEach` instead of `beforeResolve`.** If you fetch data in `beforeEach`, the async component has not loaded yet. The fetch starts before the route is confirmed. Use `beforeResolve` for data that belongs to the resolved component. ### Real-world usage - Nuxt 3 wraps guards as `defineNuxtRouteMiddleware`, handling SSR vs client differences (server reads cookies via `useRequestHeaders()`, client uses `localStorage`) - Quasar Framework registers global guards in `boot` files for PWA offline checks before navigation - Pinia auth stores centralize guard logic: `useAuthStore().check()` inside `beforeEach` keeps token validation in one place - VitePress uses `beforeEach` hooks during navigation to trigger search index updates ### Follow-up questions **Q:** What is the full guard execution order? **A:** `router.beforeEach` → route `beforeEnter` → async component resolution → `router.beforeResolve` → navigation commit → `router.afterEach`. **Q:** When do you use `beforeResolve` instead of `beforeEach`? **A:** `beforeEach` runs before any component loads, so use it for redirects and auth checks. `beforeResolve` runs after components are resolved but before render, making it the right place for data fetches tied to the component. **Q:** How does Vue Router handle async guards? **A:** Return a Promise from the guard. The router awaits it. If you use the `next` callback, call `next()` only after your async work completes. **Q:** How do you access route meta in a guard? **A:** Via `to.meta.yourField`. Add any property to the `meta` object in the route config and it is available inside all guards, including global ones. **Q:** (Senior) In Nuxt SSR, how do you prevent guard logic from breaking on the server? **A:** Server-side code cannot access `localStorage`. Use `process.client` to guard client-only code, and use `useRequestHeaders()` to read auth cookies on the server. Nuxt middleware with `defineNuxtRouteMiddleware` handles this separation cleanly. **Q:** (Senior) A user clicks logout while a `beforeEach` guard is mid-flight on an async profile fetch. What breaks and how do you fix it? **A:** The guard's `await` resolves after logout, so auth state is stale by the time the check runs and the navigation may still proceed. Fix: use `AbortController` on the fetch and re-check `authStore.isLoggedIn` after the `await`. If false, return `/login` immediately. ## Examples ### Auth guard with redirect preservation ```typescript // router/index.ts import { createRouter, createWebHistory } from 'vue-router' import { useAuthStore } from '@/stores/auth' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: () => import('@/views/Home.vue') }, { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/profile/:id', component: () => import('@/views/UserProfile.vue'), meta: { requiresAuth: true } } ] }) router.beforeEach((to) => { const auth = useAuthStore() if (to.meta.requiresAuth && !auth.isLoggedIn) { // Preserve the intended path for post-login redirect return { path: '/login', query: { redirect: to.fullPath } } } }) export default router ``` Visiting `/profile/42` without auth redirects to `/login?redirect=/profile/42`. The login page reads `route.query.redirect` and sends the user straight to `/profile/42` after a successful login. ### Per-route async role check ```typescript const routes = [ { path: '/dashboard', component: () => import('@/views/Dashboard.vue'), beforeEnter: async (to, from) => { const user = await fetchUser() // only runs when /dashboard is accessed if (!user.isAdmin) return '/login' } } ] ``` The API call only happens when someone visits `/dashboard`. Non-admins land on `/login`. The guard lives on the route config, so the global `beforeEach` stays clean and focused on auth only. ### Nested routes with shared layout ```typescript const routes = [ { path: '/dashboard', component: () => import('@/layouts/DashboardLayout.vue'), meta: { requiresAuth: true }, // applies to all children automatically children: [ { path: '', component: () => import('@/views/DashboardHome.vue') }, // /dashboard { path: 'settings', component: () => import('@/views/Settings.vue') }, // /dashboard/settings { path: 'profile', component: () => import('@/views/Profile.vue') } // /dashboard/profile ] } ] ``` ```vue <!-- DashboardLayout.vue --> <template> <div class="dashboard-layout"> <Sidebar /> <main> <RouterView /> <!-- child route components render here --> </main> </div> </template> ``` `meta: { requiresAuth: true }` on the parent propagates to all children. One guard check in `beforeEach` covers the entire dashboard section without repeating the meta field on every child route.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.