In this article, we will build a client-side routing system. Client-side routing is a type of routing where users navigate through an application where no full page reload occurs even when the page’s URL changes — instead, it displays new content.
To build this, we’ll need a simple server that will serve our index.html
file. Ready? Let’s begin.
First, set up a new node.js application and create the project structure:
npm init -y npm install express morgan nodemon --save touch server.js mkdir public && cd public touch index.html && touch main.js file cd ..
The npm init
command will create a package.json
file for our application. We’ll install Express
and Morgan
, which will be used in running our server and logging of our routes.
We’ll also create a server.js
file and a public directory where we will be writing our views. Nodemon will restart our application once we make any change in our files.
Let’s create a simple server using Express by modifying the server.js
file:
const express = require('express'); const morgan = require('morgan'); const app = express(); app.use(morgan('dev')); app.use(express.static('public')) app.get('*', (req, res) => { res.sendFile(__dirname + '/public/index.html') }) app.listen(7000, () => console.log("App is listening on port 7000"))
Now we can start our application by running nodemon server.js
. Let’s create a simple boilerplate for our HTML:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h1>Javascript Routing</h1> <div id="app"> </div> <script src="main.js"></script> </body> </html>
Here, we’ll link the main.js
file so that we can manipulate the DOM at any point in time.
Let’s head over to the main.js
file and write all of our router logic. All our codes will be wrapped in the window.onload
so that they only execute the script once the webpage has completely loaded all of its content.
Next, we’ll create a router instance that’s a function with two parameters. The first parameter will be the name of the route and the second will be an array which comprises all our defined routes. This route will have two properties: the name of the route and the path of the route.
window.onload = () => { // get root div for rendering let root = document.getElementById('app'); //router instance let Router = function (name, routes) { return { name, routes } }; //create the route instance let routerInstance = new Router('routerInstance', [{ path: "/", name: "Root" }, { path: '/about', name: "About" }, { path: '/contact', name: "Contact" } ]) }
We can get the current route path of our page and display a template based on the route.location.pathname
returns the current route of a page, and we can use this code for our DOM:
let currentPath = window.location.pathname; if (currentPath === '/') { root.innerHTML = 'You are on Home page' } else { // check if route exist in the router instance let route = routerInstance.routes.filter(r => r.path === currentPath)[0]; if (route) { root.innerHTML = `You are on the ${route.name} path` } else { root.innerHTML = `This route is not defined` } }
We’ll use the currentPath
variable to check if a route is defined in our route instance. If the route exists, we’ll render a simple HTML template. If it doesn’t, we’ll display This route is not defined
on the page.
Feel free to display any form of error of your choice. For example, you could make it redirect back to the homepage if a route does not exist.
For navigation through the pages, we can add router links. Just like with Angular, you can pass a routerLink
that will have a value of the path you want to navigate to. To implement this, let’s add some links to our index.html
file :
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <nav> <button router-link="/">Home</button> <button router-link="/about">About</button> <button router-link="/contact">Contact</button> <button router-link="/unknown">Error</button> </nav> <h1>Javascript Routing</h1> <div id="app"> </div> <script src="main.js"></script> </body> </html>
Notice the router-link
attribute that we passed in — this is what we will be using for our routing.
We’ll create a variable store all of router-link
s and store it in an array:
let definedRoutes = Array.from(document.querySelectorAll('[router-link]'));
After storing our router-links in an array, we can iterate through them and add a click event listener that calls the navigate()
function:
//iterate over all defined routes definedRoutes.forEach(route => { route.addEventListener('click', navigate, false) })
The navigate function will be using Javascript History API to navigate. The history.pushState()
method adds a state to the browser’s session history stack.
When the button is clicked, we’ll receive the router link attribute of that button and then use the history.pushState()
to navigate to that path, then change the HTML template rendered:
// method to navigate let navigate = e => { let route = e.target.attributes[0].value; // redirect to the router instance let routeInfo = routerInstance.routes.filter(r => r.path === route)[0] if (!routeInfo) { window.history.pushState({}, '', 'error') root.innerHTML = `This route is not Defined` } else { window.history.pushState({}, '', routeInfo.path) root.innerHTML = `You are on the ${routeInfo.name} path` } }
If a nav link has a router link that has not been defined in the routeInstance
, it will set the push state to error
and render This route is not Defined
on the template.
Next, you should consider storing routes in a separate file, which makes codes neater and easier to debug if there are any errors. Now, create a routes.js
file and extract the route constructor and router instance into this new file:
//router instance let Router = function (name, routes) { return { name, routes } }; let routerInstance = new Router('routerInstance', [{ path: "/", name: "Root" }, { path: '/about', name: "About" }, { path: '/contact', name: "Contact" } ]) export default routerInstance
Exporting this file makes it accessible to other JavaScript files. We can import it into our main.js file:
import routerInstance from './routes.js'
This will throw an error. To fix it, modify the script tag in the index.html file to this:
<script type="module" src="main.js"></script>
Adding the type of module specifies which variables and functions can be accessed outside the modules.
Understanding how to implement a routing system in Vanilla JavaScript makes it easier for developers to work with a framework routing library such as the Vue.js Router. Our code here can be reused in a single page application, which is perfect when you’re working without a framework. To get the source code, check out GitHub.
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Would you be interested in joining LogRocket's developer community?
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 nowReact Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
6 Replies to "Building a JavaScript router using History API"
Interesting Read👏🏽
Overly simplified. Awesome
This is very enlightening. Great article Wisdom.
This is very helpful for my current project. Thank you
Awesome content man!
Interesting. Can I also make it reactive in this way, detecting a change in window.location? how would I do that?