intermediate
frontend

Vue Router

Dynamic routes, navigation guards, lazy-loaded views, and route meta

vue fundamentals
24 min read

Vue Router

Vue Router is the official client-side routing library for Vue. It maps URLs to components so a single-page application can render different views, keep the browser address bar in sync, support the back/forward buttons, and let users deep-link or bookmark any screen — all without a full page reload. Once your app grows past a couple of screens, hand-rolling this with v-if becomes unmanageable, and Vue Router gives you dynamic segments, nested layouts, code-splitting, and route-level access control out of the box.

Learning Objectives
  • Create a router with createRouter and wire it into your app
  • Define dynamic and nested routes and read their params
  • Navigate declaratively and programmatically with RouterLink and useRouter
  • Lazy-load route components to shrink the initial bundle
  • Protect routes with navigation guards and route meta fields

Setting Up the Router

Vue Router 4 pairs with Vue 3. Install it, create a router instance with a history mode, register your route table, and install it as a plugin.

bash
npm install vue-router@4
ts
// src/router/index.ts import { createRouter, createWebHistory } from "vue-router" import HomeView from "@/views/HomeView.vue" const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: "/", name: "home", component: HomeView }, { path: "/about", name: "about", component: () => import("@/views/AboutView.vue") }, ], }) export default router
ts
// src/main.ts import { createApp } from "vue" import App from "./App.vue" import router from "./router" createApp(App).use(router).mount("#app")

In your root component you render the matched view with <RouterView> and link to routes with <RouterLink> — both are registered globally once the plugin is installed.

vue
<template> <nav> <RouterLink to="/">Home</RouterLink> <RouterLink :to="{ name: 'about' }">About</RouterLink> </nav> <RouterView /> </template>

History modes

ts
import { createWebHistory } from "vue-router" // Clean URLs: /about // Requires a server fallback that serves index.html for unknown paths. const history = createWebHistory()

Dynamic Routes and Params

A colon marks a dynamic segment. /users/:id matches /users/42, and the value is exposed on route.params. Inside <script setup> you read the current route with useRoute().

vue
<script setup lang="ts"> import { useRoute } from "vue-router" const route = useRoute() // route.params.id is a string: "42" </script> <template> <h1>User #{{ route.params.id }}</h1> </template>

Passing props: true decouples the component from the router by turning params into component props — much easier to test and reuse.

ts
{ path: "/users/:id", name: "user", component: () => import("@/views/UserView.vue"), props: true, }
vue
<script setup lang="ts"> const props = defineProps<{ id: string }>() </script>
ts
import { watch } from "vue" import { useRoute } from "vue-router" const route = useRoute() watch(() => route.params.id, (id) => fetchUser(id), { immediate: true })

Nested Routes

Real apps have layouts inside layouts — a settings page with profile, security, and billing tabs. children builds that hierarchy, and each parent needs its own <RouterView> where children render.

ts
{ path: "/settings", component: () => import("@/views/SettingsLayout.vue"), children: [ { path: "", name: "settings", redirect: { name: "profile" } }, { path: "profile", name: "profile", component: () => import("@/views/ProfileTab.vue") }, { path: "security", name: "security", component: () => import("@/views/SecurityTab.vue") }, ], }

Note that child path values are relative and must not start with a slash — profile resolves to /settings/profile.

Programmatic Navigation

useRouter() returns the router instance for navigating in response to logic — after a form submit, a login, or a timeout. Use push to add a history entry and replace when you do not want the current page in the back stack.

vue
<script setup lang="ts"> import { useRouter } from "vue-router" const router = useRouter() async function onLogin() { await authStore.login(form) // Object form supports name, params, and query router.push({ name: "user", params: { id: authStore.userId } }) } function goBack() { router.back() } function redirectToDashboard() { // No back-button entry for the login page router.replace({ path: "/dashboard", query: { welcome: "true" } }) } </script>

Lazy-Loaded Views and Route Meta

Passing a dynamic import() as component tells your bundler to split that view into its own chunk, fetched only when the route is visited. This keeps the initial JavaScript payload small — critical as an app grows. The meta field attaches arbitrary data to a route, most commonly access rules.

ts
{ path: "/admin", name: "admin", // Separate chunk, loaded on demand component: () => import("@/views/AdminView.vue"), meta: { requiresAuth: true, roles: ["admin"] }, }

For full type safety, augment the RouteMeta interface so meta.requiresAuth is checked at compile time.

ts
// src/router/meta.d.ts import "vue-router" declare module "vue-router" { interface RouteMeta { requiresAuth?: boolean roles?: string[] } }

Guards run before a navigation is confirmed, letting you cancel it, redirect, or wait on async work. A global beforeEach guard is the standard place to enforce authentication. In Vue Router 4 you return the result — false to abort, a route location to redirect, or nothing/true to allow.

ts
router.beforeEach((to) => { const auth = useAuthStore() if (to.meta.requiresAuth && !auth.isLoggedIn) { // Redirect to login and remember where the user wanted to go return { name: "login", query: { redirect: to.fullPath } } } if (to.meta.roles && !to.meta.roles.some((r) => auth.roles.includes(r))) { return { name: "forbidden" } } })

Guards come in three scopes. Reach for the narrowest one that fits.

ts
// Runs for every navigation — auth, analytics, page titles. router.afterEach((to) => { document.title = to.meta.title ?? "My App" })

Scroll Behavior

For a native-feeling app, control the scroll position on navigation. scrollBehavior restores the saved position on back/forward and scrolls to the top (or to an anchor) otherwise.

ts
const router = createRouter({ history: createWebHistory(), routes, scrollBehavior(to, from, savedPosition) { if (savedPosition) return savedPosition if (to.hash) return { el: to.hash, behavior: "smooth" } return { top: 0 } }, })

Common Pitfalls

Next Steps

Continue Learning
Optimize renders.

Next Lesson

Vue Performance Optimization