skip to content

Navigation Guards

Global, per-route and in-component guards, what each return value does, and the order they run in. Interviewers ask for that order and a login redirect that keeps the destination.

on this pageshow

explore

questions

5

In Vue Router 5, how do you write a `router.beforeEach` guard that uses `to.meta.requiresAuth` to send signed-out HR-portal users to `/login` with their destination?

level: juniorimportance: must knowfreq 72%

answer

  1. one global guard, every navigation
  2. meta on the route record
  3. to.meta merges parent into child
  4. return a location to redirect
  5. keep to.fullPath in the query

basics

~20 s

Register router.beforeEach((to) => …) and, when to.meta.requiresAuth is set and nobody is signed in, return { name: 'login', query: { redirect: to.fullPath } }. Returning nothing lets every other navigation through; keep the login route itself unprotected.

solid answer

~40 s

`router.beforeEach` registers a global guard that runs before every navigation, in registration order, with the target `to` and the current `from`. Route records carry a `meta` object, and `to.meta` merges the matched records' meta from parent to child, so `requiresAuth: true` on the `/hr` parent protects all its children. The guard reads the session, for example from an auth store called inside the guard, and returns `{ name: 'login', query: { redirect: to.fullPath } }` for a signed-out user; returning nothing lets the navigation continue. `to.fullPath` keeps path, query and hash, and the login page later pushes it back after checking it is an in-app path. The login route must stay unprotected, or the guard redirects to itself forever; in development the router aborts such a loop with a warning.

code

ts · 19 lines
ts
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from './routes'
import { useAuthStore } from '@/stores/auth'

declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    roles?: string[]
  }
}

export const router = createRouter({ history: createWebHistory(), routes })

router.beforeEach((to) => {
  const auth = useAuthStore()
  if (to.meta.requiresAuth && !auth.isSignedIn) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }
})

go deeper

for a junior

Recall that router.beforeEach runs before every navigation with to and from, that returning nothing continues, and that returning a location such as the login route redirects.

for a middle

Explain meta on route records, the parent-to-child merge into to.meta, carrying to.fullPath in the query, and why the login route must stay out of the guard's reach.

for a senior

Show you avoid the redirect loop, read stores inside the guard's app context, validate the redirect target before replacing to it, and treat the guard as UX, not security.

for a principal

Decide how access rules are declared across many teams' routes, typed meta conventions versus central tables, so one guard stays small and auditable.

## What `router.beforeEach` is In Vue Router 5, `router.beforeEach(guard)` registers a **global before guard**: a function the router calls before every navigation, whether it starts from a RouterLink, `router.push()`, the Back button or the first page load. Global before guards run in the order they were registered, and a navigation stays **pending** until all of them have resolved. The call returns a function that removes the guard again. Each guard receives two route locations: - **`to`**: where the navigation is going, already resolved, with `path`, `fullPath`, `params`, `query`, `name`, `matched` and `meta`; - **`from`**: where the user is now. The guard answers by what it returns. Returning nothing lets the navigation continue to the next guard; returning a route location redirects. (The other answers, `false` and a thrown error, are the subject of the return-value protocol.) ## Marking routes with `meta` Which pages need a session is a property of the route, so it belongs on the route record: ```ts const routes = [ { path: '/login', name: 'login', component: LoginPage }, { path: '/hr', component: HrLayout, meta: { requiresAuth: true }, children: [ { path: 'employees', name: 'employees', component: EmployeeList }, { path: 'payroll', name: 'payroll', component: Payroll, meta: { roles: ['payroll-admin'] } }, ], }, ] ``` `to.meta` is a **shallow merge** of every matched record's `meta`, parent first, so on `/hr/payroll` it is `{ requiresAuth: true, roles: ['payroll-admin'] }`. Two consequences: | Situation | Result | |---|---| | flag on the parent only | every child inherits it through `to.meta` | | same key on parent and child | the child's value wins | | need every record's own value | read `to.matched.map(r => r.meta)` | For type safety, augment the `RouteMeta` interface of `vue-router` with `requiresAuth?: boolean` and `roles?: string[]`. ## The guard ```ts router.beforeEach((to) => { const auth = useAuthStore() if (to.meta.requiresAuth && !auth.isSignedIn) { return { name: 'login', query: { redirect: to.fullPath } } } }) ``` The store is created **inside** the guard, not at the top of `router.ts`. Since Vue 3.3 the router runs guards inside `app.runWithContext()`, so `inject()` and anything provided with `app.provide()` are available there; at module level there is no app context yet. ## Keeping the destination `to.fullPath` is the path plus query and hash, such as `/hr/payroll?month=2026-09`. Putting it in the login route's query survives a reload of the login page, unlike in-memory state. After a successful sign-in the login page reads `route.query.redirect`, checks that it is an in-app path (it starts with a single `/`), and calls `router.replace(redirect)` so Back does not return to the login form. ## Several guards, one order Larger portals often register more than one global guard, say one for sign-in and one for roles. They run in **registration order**, and the first one that redirects or cancels ends the navigation; the others never see it. So register the sign-in guard first: a role check makes no sense for a user who is not signed in. `router.beforeEach()` also returns a function that unregisters the guard, which is handy in tests and for guards installed by a feature that can be switched off. ## Avoiding the redirect loop The classic bug is a guard that protects **everything**, the login page included: 1. the user opens `/hr/employees` and is redirected to `login`; 2. the redirect is a new navigation, so the same guard runs for `login`; 3. nobody is signed in, so it redirects to `login` again, and so on. Fix it by leaving `meta.requiresAuth` off the login route, or by adding `to.name !== 'login'` to the condition. In development the router notices a guard that keeps redirecting to the location it is already navigating to and, after more than 30 rounds, aborts with a warning about a possibly infinite redirection; a production build has no such check. ## Common mistakes - Checking `from` instead of `to`: the guard must judge where the user is going. - Calling the auth store at module level, before the app and its plugins exist. - Protecting the login route and creating a redirect loop. - Storing the destination only in memory, where a reload loses it. - Trusting the client guard as security: it only shapes navigation, and the server must still refuse data to users without access.

  • Why call `useAuthStore()` or `inject()` inside the guard body rather than at the top of router.ts?
    Since Vue 3.3 the router runs every guard inside `app.runWithContext()`, so `inject()` and values from `app.provide()` resolve there, and a store finds the app's instance. Module-level code in router.ts runs when the file is imported, usually before `createApp()` and before the store plugin is installed, so there is no app context to inject from.
  • The payroll child sets `meta: { requiresAuth: false }` by mistake under a parent with `requiresAuth: true`. What does `to.meta.requiresAuth` read on payroll?
    `false`. `to.meta` merges the matched records' meta from parent to child, and the child's key overwrites the parent's. If a parent's flag must not be overridable, check `to.matched.some(r => r.meta.requiresAuth)` instead of the merged `to.meta`.

saying these in an interview costs you the question

  • to.meta holds only the matched child record's meta
  • A beforeEach guard must return true or the navigation is cancelled
  • The login route can carry requiresAuth because redirects skip guards
  • Store the destination in a component variable instead of the URL
  • A client-side beforeEach guard is enough to secure HR data
open as a page

In Vue Router 5, what happens to a navigation when a `beforeEach` guard returns nothing, returns a route location, returns `false`, or throws?

level: middleimportance: must knowfreq 66%

basics

~20 s

Nothing or true lets the navigation continue to the next guard; a route location drops it and starts a new navigation there; false cancels it and resets the URL to from; a thrown Error cancels it and calls router.onError() handlers.

open as a page

In Vue Router 5, why does `onBeforeRouteLeave` guard an employee edit form's unsaved changes on leaving, but not when switching to another employee?

level: middleimportance: should knowfreq 55%

basics

~20 s

onBeforeRouteLeave runs only when the route record rendering the form is left. /employees/7/edit to /employees/8/edit keeps the same record and component instance, so only onBeforeRouteUpdate runs; register the same check in both and return false to stay.

open as a page

In Vue Router 5, a legacy `beforeEach((to, from, next) => …)` guard logs a deprecation warning and sometimes leaves a navigation pending forever; why, and how do you migrate it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

A guard declared with a third next parameter is resolved only when next is called, so a branch that forgets it hangs the navigation; next is deprecated in Vue Router 5. Drop the parameter and return the value you passed to next.

open as a page

In Vue Router 5, when a user leaves an HR edit form for a lazily loaded payroll route with `beforeEnter`, in what order do the guards run, and where does `beforeResolve` fit?

level: seniorimportance: should knowfreq 50%

basics

~10 s

Leave guards of the edit form, global beforeEach, update guards of reused components, the payroll record's beforeEnter, the lazy component loads, its beforeRouteEnter, global beforeResolve, confirmation, afterEach, the DOM update, then next(vm) callbacks.

open as a page