skip to content

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