Vue, JavaScript, Tutorials Finding Your Route: Mastering Vue Router
6 min read
4 viewsFinding 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
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
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/aboutinstead of/#/about.use(router)makes Vue Router available everywhere in the app
router-view: The Outlet
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
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
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.
Navigation Guards
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
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
- Route config → maps URL paths to components
router-view→ the outlet where matched components renderrouter-link→ declarative navigation that stays in sync with the active routeuseRouter→ 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:
- Add a named route for
/city/:idand navigate to it from a list usingrouter.push({ name: 'city', params: { id } }) - Add a
queryparam like?unit=Cand read it withroute.query.unit - Add a
beforeEachguard 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
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.