How to Handle Routes in Vue

How to Handle Routes in Vue Vue.js has emerged as one of the most popular JavaScript frameworks for building dynamic, component-driven user interfaces. At the heart of any modern single-page application (SPA) lies routing — the mechanism that allows users to navigate between different views or pages without triggering a full page reload. In Vue, routing is handled by Vue Router, the official routi

Nov 10, 2025 - 08:43
Nov 10, 2025 - 08:43
 1

How to Handle Routes in Vue

Vue.js has emerged as one of the most popular JavaScript frameworks for building dynamic, component-driven user interfaces. At the heart of any modern single-page application (SPA) lies routing the mechanism that allows users to navigate between different views or pages without triggering a full page reload. In Vue, routing is handled by Vue Router, the official routing library designed specifically for Vue applications. Mastering how to handle routes in Vue is essential for creating seamless, scalable, and user-friendly web applications. Whether you're building a simple blog or a complex enterprise dashboard, understanding routing fundamentals ensures your app behaves predictably, loads efficiently, and provides an intuitive user experience.

This tutorial provides a comprehensive, step-by-step guide to handling routes in Vue using Vue Router. Youll learn how to set up routing from scratch, configure nested and dynamic routes, implement navigation guards, manage route parameters, and optimize performance. Well also cover best practices, essential tools, real-world examples, and answer frequently asked questions to solidify your understanding. By the end of this guide, youll have the confidence to implement robust routing in any Vue application regardless of complexity.

Step-by-Step Guide

Setting Up Vue Router in a New Project

Before you can handle routes in Vue, you must first install and configure Vue Router. The process is straightforward and varies slightly depending on whether youre using Vue 2 or Vue 3. This guide assumes youre using Vue 3, which is the current standard as of 2024.

Start by creating a new Vue project using the Vue CLI or Vite. If youre using Vite (recommended for new projects), run:

npm create vue@latest my-vue-app

During setup, select Yes when prompted to add Vue Router. If you skipped this step or are adding Vue Router to an existing project, install it manually:

npm install vue-router

Once installed, create a new file in your src directory called router/index.js. This will be your central routing configuration file.

Inside router/index.js, import the necessary modules and define your routes:

import { createRouter, createWebHistory } from 'vue-router'

import Home from '../views/Home.vue'

import About from '../views/About.vue'

import Contact from '../views/Contact.vue'

const routes = [

{

path: '/',

name: 'Home',

component: Home

},

{

path: '/about',

name: 'About',

component: About

},

{

path: '/contact',

name: 'Contact',

component: Contact

}

]

const router = createRouter({

history: createWebHistory(),

routes

})

export default router

Here, were using createWebHistory() to enable HTML5 history mode, which produces clean URLs without the hash symbol (

). This is preferred for production applications because it improves SEO and provides a more native browsing experience.

Next, import the router into your main application file (src/main.js) and use it:

import { createApp } from 'vue'

import App from './App.vue'

import router from './router'

const app = createApp(App)

app.use(router) app.mount('

app')

Now your Vue application is configured to handle routing. The next step is to display the routed components in your UI.

Displaying Routes with Router View

To render the component associated with the current route, you must use the <RouterView> component in your root App.vue file. This acts as a placeholder that dynamically renders the component matching the current URL path.

Open src/App.vue and replace its content with:

<template>

<nav>

<router-link to="/">Home</router-link> |

<router-link to="/about">About</router-link> |

<router-link to="/contact">Contact</router-link>

</nav>

<RouterView />

</template>

<script>

import { RouterLink, RouterView } from 'vue-router'

export default {

components: {

RouterLink,

RouterView

}

}

</script>

<style>

nav {

padding: 1rem; background-color:

f4f4f4;

}

nav a {

margin-right: 1rem;

text-decoration: none; color:

333;

}

nav a.router-link-active {

font-weight: bold; color:

007bff;

}

</style>

The <RouterLink> component generates anchor tags (<a>) with the correct href attributes. It also automatically applies the router-link-active class to the currently active link, which you can style to indicate the selected page.

When a user clicks a link, Vue Router intercepts the navigation, updates the URL, and renders the corresponding component inside <RouterView> all without reloading the page.

Creating Route Components

Each route points to a Vue component. Create the components referenced in your route configuration inside the src/views directory.

For example, create src/views/Home.vue:

<template>

<div>

<h2>Welcome to the Home Page</h2>

<p>This is the main landing page of your application.</p>

</div>

</template>

<script>

export default {

name: 'Home'

}

</script>

Repeat this process for About.vue and Contact.vue. Each component can contain its own template, logic, and styles independent of other pages.

Dynamic Route Parameters

Many applications need to display content based on dynamic values in the URL such as product IDs, user profiles, or blog post slugs. Vue Router supports dynamic segments using colons (:) in the route path.

For example, to create a route that displays a user profile based on a username:

{

path: '/user/:username',

name: 'UserProfile',

component: UserProfile

}

Now, visiting /user/john will render the UserProfile component. To access the dynamic parameter inside the component, use the useRoute() composable:

<template>

<div>

<h2>Profile of {{ username }}</h2>

<p>User ID: {{ userId }}</p>

</div>

</template>

<script>

import { useRoute } from 'vue-router'

export default {

name: 'UserProfile',

setup() {

const route = useRoute()

const username = route.params.username

const userId = route.params.id // if passed via query or additional param

return { username, userId }

}

}

</script>

You can also define multiple dynamic segments:

{ path: '/post/:id/comments/:commentId', component: PostComments }

Access them as route.params.id and route.params.commentId.

Query Parameters and URL Search Strings

Query parameters appear after the ? in a URL (e.g., /search?q=vue&sort=latest). Unlike route parameters, they are optional and dont affect route matching.

To access query parameters, use route.query:

const route = useRoute()

console.log(route.query.q) // "vue"

console.log(route.query.sort) // "latest"

To navigate with query parameters, use the to prop in <RouterLink>:

<RouterLink :to="{ path: '/search', query: { q: 'vue', sort: 'latest' } }">Search Vue</RouterLink>

Or programmatically using router.push():

import { useRouter } from 'vue-router'

const router = useRouter()

router.push({ path: '/search', query: { q: 'vue', sort: 'latest' } })

Nested Routes

Complex UIs often require hierarchical layouts for example, a dashboard with sidebar navigation and content areas. Vue Router supports nested routes to handle such structures.

Define a parent route with a children array:

{

path: '/dashboard',

name: 'Dashboard',

component: DashboardLayout,

children: [

{

path: '',

component: DashboardHome

},

{

path: 'settings',

component: Settings

},

{

path: 'profile',

component: Profile

}

]

}

Here, /dashboard renders DashboardLayout, which must include a <RouterView> to render its child routes:

<template>

<div class="dashboard">

<nav>

<RouterLink to="/dashboard">Home</RouterLink>

<RouterLink to="/dashboard/settings">Settings</RouterLink>

<RouterLink to="/dashboard/profile">Profile</RouterLink>

</nav>

<main>

<RouterView />

</main>

</div>

</template>

Now, visiting /dashboard/settings renders Settings inside the DashboardLayouts <RouterView>.

Programmatic Navigation

While <RouterLink> is ideal for declarative navigation, you often need to trigger route changes programmatically for example, after form submission or authentication.

Use the useRouter() composable to access the router instance:

import { useRouter } from 'vue-router'

export default {

setup() {

const router = useRouter()

const handleLogin = () => {

// After successful login

router.push('/dashboard')

}

const handleLogout = () => {

router.push('/login')

}

const goBack = () => {

router.go(-1) // Navigate back one page

}

return { handleLogin, handleLogout, goBack }

}

}

You can also use router.replace() to replace the current entry in the history stack (useful for redirects), or router.go(n) to move forward or backward in history.

Redirects and Wildcard Routes

Sometimes you need to redirect users from one route to another for example, from the root path to a default page, or from an old URL to a new one.

Add a redirect route:

{

path: '/',

redirect: '/home'

},

{

path: '/old-page',

redirect: '/new-page'

}

To catch all unmatched routes (404 pages), use a wildcard route:

{

path: '/:pathMatch(.*)*',

name: 'NotFound',

component: NotFound

}

The /:pathMatch(.*)* pattern matches any URL that doesnt correspond to a defined route. Place this route at the end of your routes array to ensure it only triggers as a fallback.

Route Meta Fields and Data

Vue Router allows you to attach custom metadata to routes using the meta property. This is useful for storing information like page titles, required authentication, or UI flags.

{

path: '/admin',

component: AdminPanel,

meta: {

requiresAuth: true,

title: 'Admin Dashboard'

}

}

Access meta fields in components or navigation guards:

const route = useRoute()

if (route.meta.requiresAuth) {

// Check authentication

}

You can also use meta to dynamically update the page title:

import { onMounted } from 'vue'

import { useRoute } from 'vue-router'

export default {

setup() {

const route = useRoute()

onMounted(() => {

document.title = route.meta.title || 'My App'

})

}

}

Best Practices

Organize Routes in a Modular Structure

As your application grows, your routes file can become unwieldy. To maintain scalability, split routes into feature-based modules. For example:

  • src/router/modules/auth.js routes for login, register, password reset
  • src/router/modules/dashboard.js routes for admin and user dashboards
  • src/router/modules/products.js routes for product listings, details, categories

Then import and combine them in your main router file:

import { createRouter, createWebHistory } from 'vue-router'

import authRoutes from './modules/auth'

import dashboardRoutes from './modules/dashboard'

import productRoutes from './modules/products'

const routes = [

...authRoutes,

...dashboardRoutes,

...productRoutes,

{ path: '/:pathMatch(.*)*', component: NotFound }

]

const router = createRouter({

history: createWebHistory(),

routes

})

export default router

This approach improves readability, enables team collaboration, and simplifies testing.

Lazy-Load Components for Performance

Loading all route components at application startup increases initial bundle size and slows down load times. Vue Router supports code-splitting via dynamic imports, which loads components only when the route is accessed.

Replace static imports with import():

const routes = [

{

path: '/',

component: () => import('../views/Home.vue')

},

{

path: '/about',

component: () => import('../views/About.vue')

},

{

path: '/contact',

component: () => import('../views/Contact.vue')

}

]

Vue CLI and Vite automatically split these into separate chunks, which are fetched on-demand. This dramatically improves initial load performance especially for large applications.

Use Navigation Guards for Authorization and Validation

Navigation guards are functions that Vue Router calls during navigation transitions. They allow you to control access to routes based on conditions like authentication, permissions, or form validation.

Use beforeEach globally to check if a user is logged in:

router.beforeEach((to, from, next) => {

const isAuthenticated = localStorage.getItem('token')

if (to.meta.requiresAuth && !isAuthenticated) {

next('/login')

} else {

next()

}

})

Use beforeEnter on individual routes for route-specific guards:

{

path: '/admin',

component: AdminPanel,

beforeEnter: (to, from, next) => {

if (to.meta.role !== 'admin') {

next('/unauthorized')

} else {

next()

}

}

}

Use beforeEach inside components to control navigation from within the component:

export default {

name: 'EditForm',

beforeRouteLeave(to, from, next) {

if (this.formDirty) {

const answer = confirm('You have unsaved changes. Are you sure you want to leave?')

if (answer) {

next()

} else {

next(false)

}

} else {

next()

}

}

}

Always call next() in guards otherwise, navigation will be blocked indefinitely.

Handle Loading States and Error Boundaries

When lazy-loading components, theres a brief delay before the component is fetched and rendered. Display a loading indicator to improve perceived performance.

Wrap your <RouterView> in a component that tracks loading state:

<template>

<div>

<div v-if="loading">Loading...</div>

<RouterView v-else />

</div>

</template>

<script>

import { RouterView, useRouter } from 'vue-router'

import { ref } from 'vue'

export default {

components: {

RouterView

},

setup() {

const loading = ref(false)

const router = useRouter()

router.beforeEach(() => {

loading.value = true

})

router.afterEach(() => {

loading.value = false

})

return { loading }

}

}

</script>

For error handling, create a dedicated error component and use the error event in your main app to catch unhandled route errors:

router.onError((error) => {

console.error('Route error:', error)

// Log to analytics, show notification, etc.

})

Optimize SEO with Dynamic Titles and Meta Tags

Search engines rely on page titles and meta descriptions to index content. Since Vue is a client-side framework, you must update these dynamically based on the route.

Use a composable to manage document metadata:

// src/composables/useMeta.js

import { onMounted, onUnmounted } from 'vue'

export function useMeta(title, description) {

onMounted(() => {

document.title = title

const metaDescription = document.querySelector('meta[name="description"]')

if (metaDescription) {

metaDescription.setAttribute('content', description)

} else {

const newMeta = document.createElement('meta')

newMeta.name = 'description'

newMeta.content = description

document.head.appendChild(newMeta)

}

})

onUnmounted(() => {

// Optionally reset to default on unmount

})

}

Then use it in your components:

import { useMeta } from '@/composables/useMeta'

export default {

setup() {

useMeta('About Us | My App', 'Learn more about our mission and values.')

}

}

For advanced SEO, consider using Vue Uses useTitle or integrating with libraries like vue-meta (for Vue 2) or server-side rendering (SSR) with Nuxt.js.

Test Your Routes

Unit and end-to-end testing of routing behavior ensures your app works as expected. Use tools like Vue Test Utils and Cypress to simulate navigation and verify component rendering.

Example with Vue Test Utils:

import { mount } from '@vue/test-utils'

import { createRouter, createWebHistory } from 'vue-router'

import Home from '@/views/Home.vue'

const router = createRouter({

history: createWebHistory(),

routes: [{ path: '/', component: Home }]

})

test('renders Home component on root path', async () => {

const wrapper = mount(App, {

global: {

plugins: [router]

}

})

await router.push('/')

await router.isReady()

expect(wrapper.findComponent(Home).exists()).toBe(true)

})

Tools and Resources

Official Vue Router Documentation

The most authoritative source for learning Vue Router is the official documentation at https://router.vuejs.org/. It includes detailed API references, code examples, migration guides, and advanced patterns.

Vue DevTools

The Vue DevTools browser extension (available for Chrome and Firefox) includes a dedicated Router tab that visualizes your route tree, current path, and navigation history. This is invaluable for debugging routing issues in real time.

Vue CLI and Vite

While Vue CLI is being phased out, Vite is now the recommended build tool for Vue 3 projects. It offers faster development server startup, instant HMR (Hot Module Replacement), and seamless integration with Vue Routers code-splitting features.

Vue Use

VueUse is a collection of essential Vue composables. It includes utilities like useTitle, useRoute, and useRouter that simplify common tasks and reduce boilerplate code.

Code Splitting and Bundle Analysis

Use tools like webpack-bundle-analyzer or vite-bundle-visualizer to visualize your JavaScript bundles. This helps identify large route components that may benefit from further code-splitting.

Routing Libraries for Special Use Cases

While Vue Router is sufficient for most applications, consider these alternatives for niche needs:

  • Nuxt.js A full-featured Vue framework with built-in routing based on file system structure. Ideal for SSR, static site generation, and SEO-heavy applications.
  • Vue 3 Router with Pinia Combine Vue Router with Pinia (Vues official state management library) to manage route-based state, such as filters or pagination parameters.

Community Resources

  • Vue Forum https://forum.vuejs.org/
  • Stack Overflow Search for vue-router questions with high upvotes
  • GitHub Repositories Explore open-source Vue apps to study real-world routing patterns

Real Examples

Example 1: E-Commerce Product Catalog

Consider an e-commerce site with these routes:

  • / Home page with featured products
  • /products List of all products
  • /products/:id Product detail page
  • /products/category/:category Filtered product list by category
  • /cart Shopping cart
  • /checkout Checkout flow

Routing configuration:

const routes = [

{ path: '/', component: Home },

{

path: '/products',

component: ProductList,

children: [

{ path: '', component: AllProducts },

{ path: 'category/:category', component: CategoryProducts }

]

},

{ path: '/products/:id', component: ProductDetail },

{ path: '/cart', component: Cart },

{ path: '/checkout', component: Checkout, meta: { requiresAuth: true } }

]

Navigation guards ensure users are logged in before accessing checkout:

router.beforeEach((to, from, next) => {

if (to.meta.requiresAuth && !authStore.isLoggedIn) {

next('/login?redirect=' + to.path)

} else {

next()

}

})

Product detail pages use dynamic parameters to fetch data:

import { useRoute } from 'vue-router'

import { ref, onMounted } from 'vue'

export default {

setup() {

const route = useRoute()

const product = ref(null)

onMounted(async () => {

const response = await fetch(/api/products/${route.params.id})

product.value = await response.json()

})

return { product }

}

}

Example 2: Admin Dashboard with Role-Based Access

Admin dashboards often have multiple user roles (admin, editor, viewer). Each role has access to different routes.

Route definitions:

const routes = [

{

path: '/dashboard',

component: DashboardLayout,

children: [

{ path: '', component: DashboardHome },

{

path: 'users',

component: ManageUsers,

meta: { role: 'admin' }

},

{

path: 'content',

component: ManageContent,

meta: { role: ['admin', 'editor'] }

},

{

path: 'analytics',

component: ViewAnalytics,

meta: { role: ['admin', 'editor', 'viewer'] }

}

]

}

]

Global guard to check roles:

router.beforeEach((to, from, next) => {

const userRole = localStorage.getItem('role')

const requiredRole = to.meta.role

if (requiredRole) {

if (Array.isArray(requiredRole)) {

if (requiredRole.includes(userRole)) {

next()

} else {

next('/unauthorized')

}

} else if (requiredRole === userRole) {

next()

} else {

next('/unauthorized')

}

} else {

next()

}

})

Example 3: Multi-Language Site with Route Prefixes

To support multiple languages, prefix routes with locale codes:

  • /en/home
  • /es/home
  • /fr/home

Configure routes with a dynamic prefix:

const routes = [

{

path: '/:lang',

component: LanguageLayout,

children: [

{ path: 'home', component: Home },

{ path: 'about', component: About },

{ path: 'contact', component: Contact }

]

},

{

path: '/:pathMatch(.*)*',

redirect: '/en'

}

]

In LanguageLayout.vue, detect the language and load translations:

import { useRoute } from 'vue-router'

import { ref } from 'vue'

export default {

setup() {

const route = useRoute()

const lang = ref(route.params.lang || 'en')

// Load translation file based on lang

const translations = import(../locales/${lang.value}.json)

return { lang, translations }

}

}

This structure enables seamless language switching and improves SEO by serving unique URLs per language.

FAQs

What is the difference between hash mode and history mode in Vue Router?

Hash mode uses the URL fragment identifier (e.g., example.com/

/about

) to simulate routing. It works on all servers because the fragment is never sent to the server. History mode uses the HTML5 History API to create clean URLs (e.g., example.com/about) but requires server configuration to serve the same index.html for all routes. History mode is preferred for SEO and user experience, but hash mode is easier to deploy on static hosts like GitHub Pages.

Can I use Vue Router with server-side rendering (SSR)?

Yes, Vue Router works with SSR frameworks like Nuxt.js. In SSR, the router must be instantiated on the server to pre-render the correct component based on the incoming URL. Vue Router provides APIs to handle this, such as router.push() and router.resolve() on the server side.

How do I prevent duplicate navigation errors in Vue Router?

By default, Vue Router throws an error if you attempt to navigate to the current route. To suppress this, wrap your navigation calls in a try-catch block, or use the router.push() method with a catch handler:

router.push('/dashboard').catch(err => {

if (err.name !== 'NavigationDuplicated') throw err

})

Alternatively, upgrade to Vue Router 4.1+, which automatically suppresses duplicate navigation errors.

How do I pass data between routes without using query parameters?

Use a state management library like Pinia or Vuex to store shared data. For temporary data (e.g., form inputs), use the browsers sessionStorage or localStorage. Avoid passing complex data via route parameters theyre meant for identifiers, not payloads.

Why is my route not rendering the correct component?

Common causes include:

  • Missing <RouterView> in parent components
  • Incorrect path matching (e.g., typos or missing leading slashes)
  • Wildcard route placed before specific routes
  • Lazy-loaded component path is incorrect or file doesnt exist

Check the browsers DevTools Network tab to ensure the component file is being loaded, and use Vue DevTools to inspect the current route state.

How do I handle route animations or transitions?

Wrap <RouterView> in a <transition> component:

<template>

<transition name="fade" mode="out-in">

<RouterView />

</transition>

</template>

<style>

.fade-enter-active, .fade-leave-active {

transition: opacity 0.3s;

}

.fade-enter-from, .fade-leave-to {

opacity: 0;

}

</style>

You can also use CSS animation libraries like Animate.css or GSAP for more complex transitions.

Conclusion

Handling routes in Vue is a foundational skill for any developer building modern web applications. With Vue Router, you gain full control over navigation, state management, and user experience all while maintaining clean, maintainable code. From setting up basic routes to implementing advanced features like code-splitting, navigation guards, and dynamic meta tags, this guide has equipped you with the knowledge to build scalable, performant, and SEO-friendly Vue applications.

Remember: good routing isnt just about linking pages its about creating intuitive, fast, and accessible journeys for your users. Prioritize performance through lazy loading, enforce security with navigation guards, and enhance SEO with dynamic titles and structured URLs. Test your routes rigorously, organize them modularly, and always keep the user experience at the center of your design decisions.

As you continue developing with Vue, revisit this guide as a reference. The principles outlined here clarity, efficiency, and user-centric design will serve you well across projects of any size. Mastering Vue Router isnt just about learning syntax; its about understanding how users navigate digital spaces and designing systems that make that navigation seamless, reliable, and delightful.