EthicsComputerCourses
Join training +91 90532 10052

Vue Router

Vue.js · Lesson 25 of 41 Intermediate 7 min read Updated

A single-page application (SPA) loads one HTML page and then swaps views with JavaScript as the user moves around, without full page reloads. Users still expect real URLs: /products/42 should open product 42, the Back button should work and links should be shareable. Vue Router, the official router for Vue, connects URLs to components.

In this lesson you will set up Vue Router, link between pages, read URL parameters, navigate from code, nest routes, protect pages with navigation guards, load pages lazily and meet the new file-based routing in Vue Router 5.

Setting up the router

In a Vite project, create-vue can add Vue Router for you, or you install it yourself with npm install vue-router. You describe the routes (which path shows which component), create a router and install it on the app:

JavaScript
// src/router/index.js
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: "/", name: "home", component: HomeView },
    { path: "/about", name: "about", component: () => import("../views/AboutView.vue") }
  ]
});

export default router;
JavaScript
// src/main.js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

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

Two built-in components then do the work in your templates:

  • <RouterLink to="/about"> renders a link that changes the URL without reloading the page.
  • <RouterView /> is the placeholder where the component for the current URL appears.

Here is a complete small app you can run. It loads Vue Router from the CDN as a global VueRouter object:

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Vue Router</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@3.5.44/dist/vue.global.prod.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue-router@5.4.0/dist/vue-router.global.prod.js"></script>
  <style>
    nav a { margin-right: 12px; }
    .router-link-exact-active { font-weight: bold; color: #2f855a; }
  </style>
</head>
<body>
  <div id="app"></div>

  <script>
    const { createApp, computed } = Vue;
    const { createRouter, createMemoryHistory, useRoute, useRouter } = VueRouter;

    const products = [
      { id: 1, name: "Cotton saree", price: 1499 },
      { id: 2, name: "Leather chappals", price: 899 },
      { id: 3, name: "Brass diya set", price: 650 }
    ];

    const HomePage = { template: `<h2>Welcome to Priya's Handloom Store, Jaipur</h2>` };

    const ProductList = {
      setup: () => ({ products }),
      template: `
        <h2>Products</h2>
        <ul>
          <li v-for="p in products" :key="p.id">
            <RouterLink :to="{ name: 'product', params: { id: p.id } }">{{ p.name }}</RouterLink>
          </li>
        </ul>`
    };

    const ProductDetail = {
      setup() {
        const route = useRoute();
        const router = useRouter();
        const product = computed(() => products.find((p) => p.id === Number(route.params.id)));
        return { product, back: () => router.back(), goHome: () => router.push("/") };
      },
      template: `
        <div v-if="product">
          <h2>{{ product.name }}</h2>
          <p>₹{{ product.price }}</p>
        </div>
        <p v-else>No product with id {{ $route.params.id }}.</p>
        <button @click="back">Back</button> <button @click="goHome">Home</button>`
    };

    const NotFound = { template: `<h2>404: page not found</h2>` };

    const router = createRouter({
      history: createMemoryHistory(),
      routes: [
        { path: "/", component: HomePage },
        { path: "/products", component: ProductList },
        { path: "/products/:id", name: "product", component: ProductDetail },
        { path: "/:pathMatch(.*)*", component: NotFound }
      ]
    });

    const App = {
      setup() {
        const route = useRoute();
        return { route };
      },
      template: `
        <nav>
          <RouterLink to="/">Home</RouterLink>
          <RouterLink to="/products">Products</RouterLink>
          <RouterLink to="/products/99">Missing product</RouterLink>
          <RouterLink to="/offers">Broken link</RouterLink>
        </nav>
        <p><small>Current path: {{ route.fullPath }}</small></p>
        <RouterView />`
    };

    createApp(App).use(router).mount("#app");
  </script>
</body>
</html>

The example uses createMemoryHistory() because it runs inside the Try-it frame, so the path is kept in memory and shown under the menu. In a real app use createWebHistory() for clean URLs.

Route params, useRoute and useRouter

A segment starting with a colon, like :id in /products/:id, is a dynamic segment. It matches any value, and the value is available as route.params.id. Params are always strings, which is why the example compares with Number(route.params.id).

  • useRoute() returns the current route: path, fullPath, params, query (for ?sort=price, read route.query.sort), name and meta. In a template you can also use $route.
  • useRouter() returns the router, for navigating from code: router.push("/products"), router.push({ name: "product", params: { id: 2 } }), router.replace(...) (no new history entry) and router.back().

Linking by name, as ProductList does, is safer than building path strings by hand: if you later change the path to /shop/:id, every named link still works.

When the user moves from /products/1 to /products/2, Vue Router reuses the same ProductDetail component instead of creating a new one. Code that reads the params once in setup() would not update. Use computed properties (as above) or a watcher such as watch(() => route.params.id, loadProduct, { immediate: true }).

The route /:pathMatch(.*)* matches anything not matched earlier, which makes it a 404 page. Click "Broken link" to see it.

Nested routes

Many layouts have pages inside pages: a dashboard with tabs, each tab with its own URL. Add children to a route and place a second <RouterView /> inside the parent component:

JavaScript
const routes = [
  {
    path: "/account",
    component: AccountLayout,       // has its own <RouterView /> inside
    children: [
      { path: "", component: AccountSummary },         // /account
      { path: "orders", component: OrderHistory },     // /account/orders
      { path: "address", component: SavedAddresses }   // /account/address
    ]
  }
];

Child paths without a leading slash are relative to the parent.

A navigation guard runs before each navigation and can allow it, cancel it or redirect it. A global beforeEach guard is the usual way to protect pages that need a login. Mark those routes with meta, then check it:

JavaScript
const routes = [
  { path: "/login", name: "login", component: LoginPage },
  { path: "/orders", component: OrdersPage, meta: { requiresAuth: true } }
];

router.beforeEach((to, from) => {
  const loggedIn = Boolean(localStorage.getItem("token"));
  if (to.meta.requiresAuth && !loggedIn) {
    return { name: "login", query: { redirect: to.fullPath } }; // redirect
  }
  // returning nothing (or true) allows the navigation; returning false cancels it
});

A front-end guard only improves the user experience. Anyone can skip it by editing the code in their browser, so your server must check permissions on every API request too.

Lazy loading and server setup

component: () => import("../views/AboutView.vue") is a lazy-loaded route. Vite puts that page in a separate JavaScript file that is downloaded only when the user first visits it, which makes the first load faster. Use lazy loading for most routes except the home page.

With createWebHistory(), URLs look normal (/products/2). If a user refreshes that page, the browser asks the server for /products/2, which does not exist as a file, and gets a 404. The server must send index.html for unknown paths. On Apache (for example XAMPP or most shared hosting), an .htaccess file in the app's folder does it:

Text
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]

If you cannot configure the server, createWebHashHistory() uses URLs like /#/products/2, which always work but look less clean and are less friendly to search engines.

Common mistakes

  • Using <a href> for internal links. A normal link reloads the whole app. Use <RouterLink>.
  • Treating params as numbers. route.params.id is a string; convert it.
  • Reading params once in setup(). The component is reused between /products/1 and /products/2; use computed properties or a watcher.
  • Forgetting the server fallback. With createWebHistory(), refreshing a deep URL needs the server to return index.html.
  • Relying only on guards for security. Always check permissions on the server.

Key points

  • Vue Router maps URLs to components: define routes, create the router with a history mode, and app.use(router).
  • <RouterLink> navigates without reloading; <RouterView> shows the matched component.
  • Dynamic segments like :id appear in useRoute().params as strings; useRouter() navigates in code.
  • children creates nested routes; beforeEach guards can redirect or cancel navigation.
  • Lazy load routes with () => import(...); configure the server to return index.html for history mode.
  • Vue Router 5 keeps the same API and adds optional file-based routing.

Exercise

Extend the store example with a /cart route and a route named category at /category/:name that lists only products in that category (add a category field such as "clothing" or "home" to each product). Add navigation links for two categories and a guard that redirects /cart to / with a message in the query string when the cart is empty.

Show solution
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Store with Cart</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@3.5.44/dist/vue.global.prod.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue-router@5.4.0/dist/vue-router.global.prod.js"></script>
</head>
<body>
  <div id="app"></div>
  <script>
    const { createApp, ref, computed } = Vue;
    const { createRouter, createMemoryHistory, useRoute } = VueRouter;

    const products = [
      { id: 1, name: "Cotton saree", price: 1499, category: "clothing" },
      { id: 2, name: "Leather chappals", price: 899, category: "clothing" },
      { id: 3, name: "Brass diya set", price: 650, category: "home" }
    ];
    const cart = ref([]);

    const HomePage = {
      setup: () => ({ route: useRoute() }),
      template: `<p v-if="route.query.msg">{{ route.query.msg }}</p><h2>Home</h2>`
    };
    const CategoryPage = {
      setup() {
        const route = useRoute();
        const items = computed(() => products.filter((p) => p.category === route.params.name));
        return { route, items, cart };
      },
      template: `
        <h2>{{ route.params.name }}</h2>
        <p v-for="p in items" :key="p.id">{{ p.name }} <button @click="cart.push(p)">Add</button></p>`
    };
    const CartPage = { setup: () => ({ cart }), template: `<h2>Cart: {{ cart.length }} item(s)</h2>` };

    const router = createRouter({
      history: createMemoryHistory(),
      routes: [
        { path: "/", component: HomePage },
        { path: "/category/:name", name: "category", component: CategoryPage },
        { path: "/cart", component: CartPage, meta: { needsItems: true } }
      ]
    });

    router.beforeEach((to) => {
      if (to.meta.needsItems && cart.value.length === 0) {
        return { path: "/", query: { msg: "Your cart is empty" } };
      }
    });

    createApp({
      template: `
        <nav>
          <RouterLink to="/">Home</RouterLink> |
          <RouterLink :to="{ name: 'category', params: { name: 'clothing' } }">Clothing</RouterLink> |
          <RouterLink :to="{ name: 'category', params: { name: 'home' } }">Home goods</RouterLink> |
          <RouterLink to="/cart">Cart</RouterLink>
        </nav>
        <RouterView />`
    }).use(router).mount("#app");
  </script>
</body>
</html>

The guard checks the route's meta and redirects to the home page with a msg query parameter, which the home page displays. Once something is in the cart, the guard lets the navigation through.

Quick Quiz

1. Which component shows the page that matches the current URL?

2. For the route /orders/:orderId, how do you read the id inside a component?

3. A user refreshes /products/2 on a site using createWebHistory() and gets a server 404. What is missing?

4. What does a beforeEach guard do when it returns { name: "login" }?

Learn Vue.js with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call