Routing in web development is a mechanism in which HTTP requests are routed to the code that handles them. In simpler terms, with a router, you determine what should happen when a user visits a certain page on your website.
In the modern age of JavaScript frameworks, routing on the web works a bit differently; you could say it’s gotten easier to implement.
In Vue, routing is handled using the Vue Router package. If your web application or website is built with Vue and contains multiple pages that users need to switch between, you definitely need Vue Router.
In this tutorial, we’re going to take a look at how to implement routing in a Vue app using Vue Router by building a mini demo application.
The demo is an application that shows information concerning breweries: their location, type, and website. We’ll be pulling data from the Open Brewery DB API.
Keep in mind we’re going to be using both the most recent versions of Vue Router and Vue in our application.
This tutorial assumes that the reader has the following installed on their machine:
Working knowledge of JavaScript and Vue is strongly recommended.
You can install the Vue CLI with the following command:
yarn global add @vue/cli # OR npm install -g @vue/cli
We will be using the Vue CLI tool to bootstrap a new Vue project. This tool allows us not to worry about configurations when we start using our app, because we can manually select the required packages.
Let’s create our project!
Open up the terminal and type in the following command to create a new Vue project:
vue create breweries-app
There’ll be a prompt to pick a preset. Select Default (Vue 3) ([Vue 3] babel, eslint) get set up quickly.
With the Vue CLI we can install the Vue Router package and let it set up automatically by choosing the Manually select features option, then choosing Vue Router as part of our project’s configuration. But that wouldn’t be much fun, would it?
For the sake of this tutorial we’ll learn how to manually set up and configure Vue Router ourselves.
Now, change directory to the project folder with the following command:
cd breweries-app
Then, start the project like so:
yarn serve #OR npm run serve
You can visit the application running on https://localhost:8080.
Before we proceed to the meat of our project, we have to work on our views. Views are the pages that will be shown to users when they visit our website or application.
First, let’s open the project in your preferred code editor.
Next, change directories on the terminal to move into the src
directory:
cd src
Then, run the following command to create a folder named views
:
mkdir views
Create the following files: AllBreweries.vue
, BreweryDetail.vue
, and NotFound.vue
inside of the views
directory we just created.
Open up the newly created files and paste in the following code specific to each file.
First, our AllBreweries.vue
page, which is the first page that will be served when a user visits our website:
<!-- Vue.Js --> <!-- AllBreweries.vue --> <template> <div class="container"> <div class="breweries-grid"> <div class="brewery" v-for="brewery in allBreweriesList" :key="brewery.id" > <div class="brewery-info"> <h3>{{ brewery.name }}</h3> <p>{{ brewery.country }}</p> <a :href="brewery.website_url"> {{ brewery.website_url || `Not available`}} </a> </div> </div> </div> </div> </template> <script> import { onMounted, ref } from "vue"; export default { setup() { const apiUrl = "https://api.openbrewerydb.org/breweries/"; // We declare our list and make it reactive let allBreweriesList = ref([]); onMounted(() => { // We call our function here when the component is first instantiated fetchAllBreweries(); }); const fetchAllBreweries = () => { // Function to retrieve a list of all breweries from the API fetch(apiUrl) .then((response) => response.json()) .then((data) => { // here we set the data gotten from the API to equal our array allBreweriesList.value = data; }); }; return { allBreweriesList, fetchAllBreweries, }; }, }; </script> <style> html { padding: 0; margin: 0; } .container { min-height: 100vh; height: 100%; margin: 0 auto; padding: 0 20px; margin: 0 auto; } .brewery { height: 200px; min-width: 280px; margin: 0 auto; border-radius: 10px; box-shadow: 1.5px 1.5px 11px hsla(208.4, 23.5%, 31.8%, 0.39), -1.5px -1.5px 11px rgba(0, 0, 0, 0); background-color: rgba(50, 155, 29, 0.568); cursor: pointer; } .breweries-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); grid-gap: 3rem; grid-auto-rows: minmax(100px, auto); padding: 50px 0; margin: 0 auto; } .brewery-info { padding-left: 20px; } </style>
In AllBreweries.vue
, we’ve successfully retrieved data from the API, then iterated over that data to display it in the template.
Next, let’s do our BreweryDetail.vue
page, which contains extra information about a specific brewery when clicked:
<!-- Vue.Js --> <!-- BreweryDetail.vue --> <template> <div class="brewery-detail"> <ul> <li><strong>Name: </strong></li> <li><strong>Type: </strong></li> <li><strong>Country: </strong></li> <li><strong>State: </strong></li> <li><strong>Street: </strong> </li> <li> <strong>Website: </strong> <a href=""></a> </li> </ul> </div> </template>
Now, in our BreweryDetail.vue
page, there isn’t much going on here other than some basic HTML. That’s because we’ll finish up this component bit by bit as we progress.
Last, let’s do some markup for our NotFound.vue
component:
<!-- Vue.Js --> <!-- NotFound.vue --> <template> <div> <h1> Sorry, this page doesn't exist or has been moved to another location </h1> </div> </template>
Our NotFound.vue
is the page we’ll serve to our users when they visit a route that does not exist. We’ll learn more about this in a later section.
Now, we’re at the fun part of installing and configuring Vue Router!
First, change directory back into the root of our project folder using this command:
cd ..
To install Vue Router in the root folder of our project, run the following:
yarn add vue-router@4 #OR npm install vue-router@4
Next, change directory back into the src
folder:
cd src
Create a folder in the src
directory called router
:
mkdir router
Change directory once again to move into the newly created router
folder:
cd router
Finally, create a file named index.js
in this directory:
touch index.js
This file will serve as our router configuration file.
Inside the newly created index.js
file, paste the following code:
// src/router/index.js import { createRouter, createWebHistory } from 'vue-router'; import AllBreweries from "@/views/AllBreweries.vue" const routes = [ { path: "/", name: "AllBreweries", component: AllBreweries, }, ] const router = createRouter({ history: createWebHistory(), routes }) export default router
Let’s dive into what our code here means.
First things first, we need to import the functions createRouter
and createWebHistory
from Vue Router. These functions create a history that a user can go back to, and construct a router object for Vue, respectively.
Notice how we create our routes
in an array, where each route is an object with the following properties:
Path
, the URL path where this route can be foundName
, an optional name to use when we link to this routeComponent
, which component to load when this route is calledWe then create the router
object to invoke the createRouter
function, then pass in the history
key values and the routes
array as parameters, then export it.
Next, open up the main.js
file in the the src
folder and add import router from "./router"
after import App from './App.vue'
, and .use(router)
between createApp(App)
and .mount('#app')
.
The bits of code above import Vue Router and instantiate it on the global Vue instance.
Next, open up the App.vue
file in the src
directory and delete all its content, leaving only the following:
<!-- Vue.Js --> <!--App.vue --> <template> <!-- This serves all our pages using vue router --> <router-view /> </template> <script> export default { name: 'App', } </script>
The router-view
component we added on line four is a functional component that renders the matched component for the given path.
Let’s rewind a bit to learn an interesting routing trick called lazy loading.
Because our apps tend to get complex as they grow, our bundle size increases, which then slows down the loading time. Thankfully, Vue Router has the lazy loading feature, which enables us to put off loading specific routes until they are visited by the user.
Let’s try to implement this in our router configuration file. In router/index.js
, add the following code after name: "AllBreweries",
:
component: () => import( /* webpackChunkName: "AllBreweries" */ '../views/AllBrewries.vue')
After you have done so, the file should look like this:
// router/index.js import { createRouter, createWebHistory } from 'vue-router'; const routes = [ { path: "/", name: "AllBreweries", component: () => import( /* webpackChunkName: "AllBreweries" */ '../views/AllBrewries.vue') } ] const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router
Notice that we’ve removed our import statement that was previously at the top and replaced the component
property for the route with a dynamic import statement. This statement fetches the required file when the route is visited. That’s all there is to it; we’ve implemented the lazy loading functionality.
For the app we’re building, when a user clicks on a specific brewery, it takes them to a page that contains more detailed information about that brewery. Now, we can’t manually create a route for every brewery that is listed in the API. How do we solve this?
Fortunately, Vue Router has a feature called dynamic routing, which enables us load dynamic data through our routes. Let’s see how we can take advantage of this.
First, we need to modify our router/index.js
file to create the route for the BreweryDetail.vue
page:
//router/index.js ... const routes = [ { path: "/", name: "AllBreweries", component: () => import( /* webpackChunkName: "AllBreweries" */ '../views/AllBrewries.vue') }, { path: "/brewery/:id", name: "BreweryDetail", component: () => import( /* webpackChunkName: "BreweryDetail" */ '../views/BreweryDetail.vue') } ...
The route’s path has a dynamic :id
segment called a “param.” The param is used to manipulate the state of the route’s component using whatever value is passed via :id
, and can be accessed using $route.params
in our template, thus making the route dynamic.
Next, let’s work on the logic behind the routing for our BreweryDetail.vue
page:
<!-- Vue.Js --> <!-- BreweryDetail.vue --> ... <script> import { onMounted, ref } from "vue"; import { useRoute } from "vue-router"; export default { setup() { // create object that brewery information from API will be stored in let breweryDetails = ref({}); const apiUrl = "https://api.openbrewerydb.org/breweries/"; // here we instantiate the useRoute method in our component const route = useRoute(); onMounted(() => { // invoke the function when our component is mounted on DOM fetchAllBreweryDetail(); }); // function to fetch all brewery information const fetchAllBreweryDetail = () => { // append the route params to the url to get information on the specific brewery clicked by user fetch(apiUrl + route.params.id) .then((response) => response.json()) .then((data) => { //set data gotten from API call to our breweryDetails Object breweryDetails.value = data; }); }; return { fetchAllBreweryDetail, breweryDetails, }; }, }; </script> ...
And we can update the template section as follows:
<!-- Vue.Js --> <!-- BreweryDetail.vue --> <template> <div class="brewery-detail"> <!-- Button users can click to back to the previous route --> <button class="back-btn" @click="$router.back()">Go back</button> <ul> <!-- Up here in the template, we access the data gotten from the API --> <li><strong>Name: </strong> {{ breweryDetails.name }} </li> <li><strong>Type: </strong> {{ breweryDetails.type || `Not available` }} </li> <li><strong>Country: </strong> {{ breweryDetails.country }} </li> <li><strong>State: </strong> {{ breweryDetails.state }} </li> <li><strong>Street: </strong> {{ breweryDetails.street || `Not available` }} </li> <li> <strong>Website: </strong> <a :href="breweryDetails.website_url"> {{ breweryDetails.website_url || `Not Available` }} </a> </li> </ul> </div> </template>
Here, our code fetches the data from the breweryDetails
object and updates it on our template.
If you’ve noticed, we’ve added a nice little button our users can click on to go back to the previously visited route. It works just like hitting the back button on the browser.
To make it possible for users to navigate to a brewery’s specific page, we’ll modify the code in the template section of AllBreweries.vue
.
Instead of our regular HTML anchor
tags, Vue Router has its own router-link
component that we can pass to the to
prop, which accepts an object with a bunch of key or value pairs.
There are two ways to navigate between routes using the router-link
component: via the path property and named routes.
In using the path property method, we only need to pass in the URL path we want users to access when they click the link. Then, append the ID retrieved from the API and set it as the param value.
The URL path is always as same as defined as in the router configuration file. Your code should look like the following (note lines 12 and 14):
<!-- Vue.Js --> <!-- AllBreweries.vue --> <template> <div class="container"> <div class="breweries-grid"> <div class="brewery" v-for="brewery in allBreweriesList" :key="brewery.id" > <div class="brewery-info"> <router-link :to="'/brewery/' + brewery.id"> <h3>{{ brewery.name }}</h3> </router-link> <p>{{ brewery.country }}</p> <a :href="brewery.website_url"> {{ brewery.website_url || `Not available`}} </a> </div> </div> </div> </div> </template>
With the named routes method, we are passing in the name
key that has access to the name
property as a value of each of the routes we created.
We also pass in the the params
key, which accepts an object. Inside the object we have the id
key, which we use to set the value of the param equal to the ID retrieved from the API.
This is what your code should look like (note lines 12, 13, and 15):
<!-- Vue.Js --> <!-- AllBreweries.vue --> <template> <div class="container"> <div class="breweries-grid"> <div class="brewery" v-for="brewery in allBreweriesList" :key="brewery.id" > <div class="brewery-info"> <router-link :to="{name: 'BreweryDetail', params: {id: brewery.id}}"> <h3>{{ brewery.name }}</h3> </router-link> <p>{{ brewery.country }}</p> <a :href="brewery.website_url"> {{ brewery.website_url || `Not available`}} </a> </div> </div> </div> </div> </template>
Both methods work, but in a scenario where we have to change the name of the URL path we want users to visit, we have to manually change it at each instance.
Now, this isn’t much of a problem here because it’s used in just one place, but think of all the stress we’d have to go through if we had used that URL path at a thousand instances.
However, if we go with the named routes method, we will only need to change the URL path at one instance: our router file configuration. Sounds easy right?
Now that we’ve covered the benefits of the named route method, we’ll be proceeding with that in this tutorial.
Say a user visits a route that we didn’t create earlier in our router configuration file. Our app will load, but without a component. We need a way to tell our router what to do when this scenario occurs.
To implement this feature, we’ll need to modify our router configuration file by adding the following code to our index.js
file:
{ path: "/:catchAll(.*)", component: () => import( /* webpackChunkName: "NotFound" */ '../views/NotFound.vue') }, ...
Vue Router uses a custom regular expression to achieve this. :catchAll
is the dynamic segment, and (.*)
is a regular expression Vue Router uses to evaluate if the route being visited has been defined in our router’s configuration file. If it doesn’t exist, our NotFound.vue
component gets rendered on the screen instead.
An interesting way to add a nice touch to our user experience is through route transitions.
To add this to our application, all we need is to wrap the <router-view/>
element tags with the v-slot
attribute around Vue Router’s custom transition
element tags.
Next, in between the transition
tags, we add Vue’s component
element and bind its is
attribute to the value of the component we get from the Vue slot.
We also pass to it a key
attribute with the value of $route.path
(the current path) to ensure that the transition works correctly, and that it will still destroy and recreate all the pages when the route changes.
In the transition
element, we assign the name
attribute the value of fade
, which determines the name of the CSS classes that will be applied to the element. The transition
element has a mode
attribute that instructs Vue Router how to handle the transition between pages.
Next, in our styles section, we can define the CSS properties that will apply to the page when the route changes.
Here’s how we can add this to our application:
<!-- Vue.Js --> <!-- App.vue --> <template> <!-- This serves all our pages using vue router --> <router-view v-slot="{ Component }"> <transition name="fade" mode="out-in"> <component :is="Component" :key="$route.path"/> </transition> </router-view> </template> <style> .fade-enter-active, .fade-leave-active { transition: opacity .4s; } .fade-enter, .fade-leave-to { opacity: 0; } </style>
Using Vue Router in our Vue applications is quite easy and straightforward. In this article we have learned how to add page routing functionalities and also utilize some of Vue Router’s features that make it simple to give our users a wonderful experience as they browse through our application.
For more information on Vue Router, I recommend reading through their documentation.
You can find the source code for this tutorial here.
Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens in your Vue apps, including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.
The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error and what state the application was in when an issue occurred.
Modernize how you debug your Vue apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.