NJ
Finding Your Route: Mastering Vue Router | Nerando Johnson
Vue.js logo on a light blue background with geometric shapes Vue, JavaScript, Tutorials

Finding Your Route: Mastering Vue Router

Nerando Johnson
#vue3 #javascript #frontend #tutorial #vue-router #composition-api

6 min read

4 views

Part 6 of 6

Vue 3 Fundamentals

This article is part of the Vue 3 Fundamentals series.

Progress 100% Complete

Finding Your Route: Mastering Vue Router

Part 6 of the Vue 3 Fundamentals series — adding client-side routing to your Vue 3 app.


A single-page app without routing is just a page.

You can build a lot with one view — but the moment your app needs a home screen, a detail view, a settings panel, or a 404 page, you need routing. Routing is what transforms a component tree into a navigable application.

Vue Router is Vue’s official solution. It’s tightly integrated with Vue’s reactivity system, plays nicely with <script setup>, and gives you everything you need — from simple path matching to dynamic segments to navigation guards — without pulling in a third-party library.

Let’s wire it up.


What Vue Router Does

Finding the right path

In a traditional multi-page site, clicking a link sends a new HTTP request and the browser loads a fresh HTML page. In a single-page app, the page never reloads. Vue Router intercepts navigation, matches the URL to a route config, and renders the matching component — all in the browser.

The URL still changes. The back button still works. Deep links still work. It just happens entirely on the client.


Installing Vue Router

Setting up the tools

If you scaffolded your project with create-vue and selected Vue Router during setup, it’s already configured. If not, add it now:

npm install vue-router@4

Then create your router file at src/router/index.js:

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'

const routes = [
  { path: '/', component: HomeView },
  { path: '/about', component: AboutView },
]

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

export default router

And register it in src/main.js:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')

Two things to notice:

  • createWebHistory() uses the HTML5 History API — clean URLs like /about instead of /#/about
  • .use(router) makes Vue Router available everywhere in the app

router-view: The Outlet

The outlet where views render

Vue Router renders the matched component into a <router-view /> tag. Think of it as a slot in your layout that gets swapped out depending on the current URL.

In src/App.vue:

<template>
  <nav>
    <router-link to="/">Home</router-link>
    <router-link to="/about">About</router-link>
  </nav>

  <router-view />
</template>

<router-link> renders as an <a> tag but intercepts the click to do client-side navigation. It also automatically adds an active class when the route matches — useful for highlighting the current nav item.

<router-view /> is where the matched component appears. When you navigate to /about, the AboutView component renders here.


Dynamic Route Segments

Dynamic routes — one path, many destinations

Static routes are fine for pages that don’t change. But most apps have detail pages — a city view, a user profile, a blog post. These need dynamic segments.

A colon (:) in a route path marks a dynamic segment:

const routes = [
  { path: '/', component: HomeView },
  { path: '/city/:id', component: CityView },
]

Now /city/atlanta, /city/new-york, and /city/seattle all match the same route. The matched value is available inside CityView via useRoute:

<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()
const cityId = route.params.id   // 'atlanta', 'new-york', etc.
</script>

<template>
  <h1>Weather for {{ cityId }}</h1>
</template>

useRoute() returns a reactive object representing the current route. route.params holds the dynamic segments, route.query holds URL query params (?unit=C), and route.path is the full path string.


Programmatic Navigation with useRouter

Navigating programmatically

router-link covers declarative navigation. For code-driven navigation — after a form submit, after an API call, on a button click — use useRouter:

<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()

function goToCity(id) {
  router.push(`/city/${id}`)
}

function goBack() {
  router.back()
}
</script>

<template>
  <button @click="goToCity('seattle')">View Seattle</button>
  <button @click="goBack()">Back</button>
</template>

router.push() adds a new entry to the history stack — the back button returns to the previous page. router.replace() swaps the current entry instead, so the back button skips over the current page. Use replace for redirects.

You can also pass an object instead of a string path:

router.push({ name: 'city', params: { id: 'seattle' } })

Named routes (name: 'city' in your route config) make programmatic navigation more refactor-safe than hardcoded strings.


Guards at the gate

Navigation guards let you run logic before a route change completes — checking authentication, logging analytics, or redirecting based on app state.

The global beforeEach guard runs before every navigation:

router.beforeEach((to, from) => {
  const isAuthenticated = !!localStorage.getItem('token')

  if (to.meta.requiresAuth && !isAuthenticated) {
    return { path: '/login' }   // redirect to login
  }
  // return nothing or true to allow the navigation
})

Mark protected routes with meta:

const routes = [
  { path: '/', component: HomeView },
  { path: '/dashboard', component: DashboardView, meta: { requiresAuth: true } },
  { path: '/login', component: LoginView },
]

Guards receive the destination route (to) and the origin route (from). Return a route location to redirect, return false to cancel, or return nothing to allow.


Seeing It All Together

All the pieces clicking into place

Here’s a mini router config for the weather dashboard we’ve been building toward:

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    component: () => import('../views/DashboardView.vue'),
  },
  {
    path: '/city/:id',
    name: 'city',
    component: () => import('../views/CityView.vue'),
  },
  {
    path: '/:pathMatch(.*)*',
    component: () => import('../views/NotFoundView.vue'),
  },
]

export default createRouter({
  history: createWebHistory(),
  routes,
})

A few things to note here:

  • () => import(...) is lazy loading — Vue only downloads a view’s code when the user navigates to it, keeping the initial bundle small.
  • /:pathMatch(.*)* is the catch-all 404 route — it matches anything that didn’t match above.

And the city detail view:

<!-- src/views/CityView.vue -->
<script setup>
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'

const route  = useRoute()
const router = useRouter()

const cityId = computed(() => route.params.id)
</script>

<template>
  <button @click="router.back()">← Back to Dashboard</button>
  <h1>{{ cityId }}</h1>
  <!-- weather data would render here -->
</template>

The Mental Model

Building the mental model
  • Route config → maps URL paths to components
  • router-view → the outlet where matched components render
  • router-link → declarative navigation that stays in sync with the active route
  • useRouter → programmatic navigation from inside <script setup>
  • useRoute → read the current URL’s params, query, and path
  • Guards → intercept navigation to check conditions before allowing or redirecting

Your Turn

Before moving to Article 6, try this:

  1. Add a named route for /city/:id and navigate to it from a list using router.push({ name: 'city', params: { id } })
  2. Add a query param like ?unit=C and read it with route.query.unit
  3. Add a beforeEach guard that logs "Navigating to: <path>" for every route change

If you can do all three, you understand how Vue Router fits into a real application.


What’s Next

What's next in the series

In Article 6, we bring it all together — building out the full weather dashboard using everything from the series: reactive state, components, and routing working as one.

You know how to move between pages. Now let’s build what lives on them.


This is Part 6 of the Vue 3 Fundamentals series. Sources: Vue Router Official Docs, Vue Router — Dynamic Route Matching.

Part 6 of 6

Vue 3 Fundamentals

This article is part of the Vue 3 Fundamentals series.

Progress 100% Complete