Skip to content

Vue Router — Routing for Vue.js Single-Page Applications

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Vue Router. We cover key concepts, practical examples, and best practices to help you master this topic.

Vue Router is the official routing library for Vue.js SPAs with nested routes, named views, navigation guards, Lazy Loading, and seamless Vue integration.

What You'll Learn

By the end of this tutorial, you will understand how to set up Vue Router, define routes with nested layouts, use navigation guards for authentication, lazy load components, and handle route transitions.

Why It Matters

Vue Router is the standard routing solution for Vue.js applications. Understanding it is essential for building SPAs with Vue that have multiple views, URL-based navigation, and complex routing requirements.

Real-World Use

An e-commerce SPA built with Vue uses Vue Router for product listing, product detail, cart, and checkout routes. Nested routes handle product categories and subcategories. Navigation guards protect the checkout route until the user is authenticated.

Basic Setup

// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';

const routes = [
    {
        path: '/',
        name: 'home',
        component: Home
    },
    {
        path: '/about',
        name: 'about',
        component: About
    },
    {
        path: '/contact',
        name: 'contact',
        component: () => import('../views/Contact.vue') // lazy load
    },
    {
        path: '/:pathMatch(.*)*',
        name: 'not-found',
        component: () => import('../views/NotFound.vue')
    }
];

const router = createRouter({
    history: createWebHistory(),
    routes,
    scrollBehavior(to, from, savedPosition) {
        if (savedPosition) return savedPosition;
        return { top: 0 };
    }
});

export default router;

// main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

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

Nested Routes

const routes = [
    {
        path: '/dashboard',
        component: () => import('../layouts/DashboardLayout.vue'),
        children: [
            {
                path: '',
                name: 'dashboard',
                component: () => import('../views/DashboardOverview.vue')
            },
            {
                path: 'users',
                name: 'users',
                component: () => import('../views/UsersList.vue')
            },
            {
                path: 'users/:id',
                name: 'user-detail',
                component: () => import('../views/UserDetail.vue'),
                props: true
            },
            {
                path: 'settings',
                name: 'settings',
                component: () => import('../views/Settings.vue'),
                meta: { requiresAuth: true }
            }
        ]
    }
];
// router/index.js
import { useAuthStore } from '../stores/auth';

const router = createRouter({ ... });

// Global guard
router.beforeEach(async (to, from, next) => {
    const authStore = useAuthStore();

    // Check if route requires authentication
    if (to.meta.requiresAuth && !authStore.isAuthenticated) {
        // Redirect to login with return URL
        next({
            name: 'login',
            query: { redirect: to.fullPath }
        });
        return;
    }

    // Check if user has required role
    if (to.meta.role && !authStore.hasRole(to.meta.role)) {
        next({ name: 'forbidden' });
        return;
    }

    next();
});

// After navigation
router.afterEach((to, from) => {
    // Update page title
    document.title = to.meta.title
        ? `${to.meta.title} — My App`
        : 'My App';

    // Send analytics
    if (typeof gtag !== 'undefined') {
        gtag('config', 'GA_MEASUREMENT_ID', {
            page_path: to.fullPath
        });
    }
});

Lazy Loading

// Lazy load all route components
const routes = [
    {
        path: '/',
        name: 'home',
        component: () => import('../views/Home.vue')
    },
    {
        path: '/products',
        name: 'products',
        component: () => import('../views/Products.vue'),
        children: [
            {
                path: ':id',
                name: 'product-detail',
                component: () => import('../views/ProductDetail.vue')
            }
        ]
    }
];

// Show loading state while component loads
// In App.vue:
// <router-view v-slot="{ Component }">
//     <transition name="fade" mode="out-in">
//         <Suspense>
//             <component :is="Component" />
//             <template #fallback>
//                 <div class="loading">Loading...</div>
//             </template>
//         </Suspense>
//     </transition>
// </router-view>

Route Transitions

// App.vue
<template>
    <router-view v-slot="{ Component, route }">
        <transition :name="route.meta.transition || 'fade'" mode="out-in">
            <component :is="Component" :key="route.path" />
        </transition>
    </router-view>
</template>

<style>
.fade-enter-active, .fade-leave-active {
    transition: opacity 0.2s ease;
}
.fade-enter-from, .fade-leave-to {
    opacity: 0;
}

.slide-enter-active, .slide-leave-active {
    transition: transform 0.3s ease;
}
.slide-enter-from {
    transform: translateX(100%);
}
.slide-leave-to {
    transform: translateX(-100%);
}
</style>

Common Mistakes

  1. Not using createWebHistory for clean URLs. Hash mode (#/path) is default. Use createWebHistory() for clean URLs and configure server fallback.
  2. Forgetting the catch-all route. Without pathMatch, unmatched routes show nothing. Always add a 404 route at the end. 3.** Not handling navigation failures.** Navigation can fail (guard rejection, component load error). Use router.onError() for global error handling.
  3. Overusing route guards for simple checks. Use component-level logic for UI conditions (showing/hiding elements) and route guards only for actual access control.
  4. Not using route names. Named routes are easier to maintain than hardcoded paths. Use router.push({ name: 'user', params: { id: 1 } }) instead of /users/1.

Practice Questions

  1. What is the difference between createWebHistory and createWebHashHistory?
  2. How do nested routes work with Vue Router?
  3. How do you implement authentication guards in Vue Router?
  4. How do you lazy load route components?
  5. How do you add page transitions between routes?

Challenge: Build a Vue 3 SPA with Vue Router: public routes (home, about, contact), authenticated dashboard with nested routes (overview, users/:id, settings), lazy-loaded components, navigation guard that redirects to login, and route transitions.

FAQ

What is the difference between Vue Router v3 and v4?

Vue Router v4 is for Vue 3. v3 is for Vue 2. v4 uses createRouter instead of new Router, has a new history API, and improved TypeScript support.

Can I use Vue Router with Vuex or Pinia?

Yes. Vue Router works alongside state management. Use route guards to check store state before navigation.

How do I handle scroll behavior in Vue Router?

Use the scrollBehavior option in createRouter(). Return position { top: 0 } for new navigations or savedPosition for back/forward.

Does Vue Router support route-level code splitting?

Yes. Use dynamic imports in the component option. Each lazy-loaded route generates a separate chunk.

How do I pass props to route components?

Set props: true in the route definition to pass route params as props. Or use props: { custom: 'value' } for static props.

Mini Project

Build a multi-page Vue 3 SPA with Vue Router: a home page, a blog section with list and detail views, an admin panel with nested routes and auth guard, lazy loading for all route components, and smooth page transitions. Use named routes and programmatic navigation.

What's Next

You mastered routing. Now learn about state management — managing shared data across your SPA components.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro