JavaScript router in 60 lines
Tags: es6, javascript, webdev
Published: 2021-11-23 16:41:50

---
title: JavaScript router in 60 lines
published: true
description: Simple way to implement JavaScript page router
tags: #ES6, #JavaScript, #webdev
//cover_image: https://direct_url_to_image.jpg
---


I was building a web application for myself and as NPM packages and JS frameworks are getting bigger and more complicated, I decided not to install some JS framework, and build the app from scratch this time.

Creating a new web-app requires Router to handle the page changes, and this is my attempt on creating one.

So what does router really do for the web application.

* The app should be able to read what URL is open and show the required content, so for example, I open a page *www.mybook.com/user/1*, the page should render the *user 1*, information.



* The page should listen to URL changes, so when I click a button or an image, that redirects the user to *www.mybook.com/post/my-latest-news* the page will not refresh, but instead removes the old content, and renders the new required content. This way of rendering content is usually called single page application or SPA.



* The page should have URL history memory, so when I press back or forward buttons in the browser, the application should know what pages to show.



* I would like the router to have a possibility to define routes and fire some action, when the user lands on that route.

For example
```javascript
router.on("/post/my-latest-news", (params) => {
  // In here, I remove old content and render new one 
})
```

* I would also like the router to accept parameters in the URL.

For example, `"/post/:id"` would give me the id value as a parameter when deciding which post to show.

That's the basic of it, I think.

For listening to listening for route change, I will use the [popstate listener](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event) API.

And for URL history, I am going to use browser [History API](https://developer.mozilla.org/en-US/docs/Web/API/History)


-----

## JavaScript Implementation

You can find the code for this router on [Github ](https://github.com/tonis2/javascript-router)

```javascript
class Router {
    constructor() {
        this.routes = new Map();
        this.current = [];
        
        // Listen to the route changes, and fire routeUpdate when route change happens.
        window.onpopstate = this.routeUpdate.bind(this);
    }
    
    // Returns the path in an array, for example URL "/blog/post/1" , will be returned as ["blog", "post", "1"]
    get path() {
        return window.location.pathname.split('/').filter((x) => x != '');
    }
    
    // Returns the pages query parameters as an object, for example "/post/?id=2" will return { id:2 } 
    get query() {
        return Object.fromEntries(new URLSearchParams(window.location.search));
    }

    routeUpdate() {
        // Get path as an array and query parameters as an object
        const path = this.path;
        const query = this.query;
        
        // When URL has no path, fire the action under "/" listener and return 
        if (path.length == 0) {
            this.routes.get('/')(path);
            return;
        }

        // When same route is already active, don't render it again, may cause harmful loops.
        if (this.current.join() === path.join()) return;
        
        // Set active value of current page
        this.current = path;
        
        // Here I save the parameters of the URL, for example "/post/:page", will save value of page
        let parameters = {};
        
        // Loop though the saved route callbacks, and find the correct action for currect URL change
        for (let [route, callback] of this.routes) {
            
            // Split the route action name into array
            const routes = route.split('/').filter((x) => x != '');
            const matches = routes
                .map((url, index) => {
                    // When the route accepts value as wildcard accept any value
                    if (url == '*') return true;

                    // Route has a parameter value, because it uses : lets get that value from the URL
                    if (url.includes(':')) {
                        parameters[url.split(':')[1]] = path[index];
                        return true;
                    }
                    // The new URL matches the saved route callback url, return true, meaning the action should be activated.
                    if (url == path[index]) return true;
                    return false;
                })
                .filter((x) => x);
            
            // When the router has found that current URL, is matching the saved route name, fire the callback action with parameters included 
            if (matches.length == routes.length && routes.length > 0) {
                callback({ path, parameters, query });
            }
        }
    }
    
    // Listen for route changes, required route name and the callback function, when route matches.
    on(route, callback) {
        this.routes.set(route, callback);
    }
    
    // Fire this function when you want to change page, for example router.change("/user/1")
    // It will also save the route change to history api.
    change(route) {
        window.history.pushState({ action: 'changeRoute' }, null, route);
        window.dispatchEvent(new Event('popstate'));
    }
}

export default new Router();

```

-----


## Using the router

**PS!**

> You should also, add ` <base href="/">` in the header of your HTML file, so the front-end router, will always start the URL path from the start, and will not keep appending to the URL.

---

First, we import the `Router`

I am going to use ES6 native modules import, it's very easy and is supported by most browsers already.

```javascript
import Router from '/libraries/router.js';
```

You can export router class from the file as new directly, or you could just do something like this 

```javascript 
window.router = new Router()
```

----


**PS!**
> My personal preference is to create the page as [webcomponent](https://developer.mozilla.org/en-US/docs/Web/Web_Components) or [lit.js](https://lit.dev) and then just swap the components when route is active.

----

```javascript

Router.on('/home', (event) => {
    // Replace and render page content here
});


Router.on('/post/:id', (event) => {
    // Replace and render page content here
    // You can get parameter with, event.parameters.id
});
```

#### Change routes

To change routes, you should use code below, because it will also store the URL change in browser history this way. 

```javascript
Router.change("/account")
```
----


## Backend setup

When creating the SPA app on web, you should be aware of an error what might happen.

When trying to load the page for a URL, for example *www.mybook.com/user/1*, the backend usually sends `404` error, page not found. 

That happens, because backend has not defined a route for `/user/1`, the route finding for it, should happen on front-end side. 

For fixing that, I redirect the 404 route on backend to `index.html` file or whatever one you are using.

So instead of backend sending route not found, it will send the SPA app main file, and then the SPA app router will render the correct page, because it has the information about the routes.

------

#### Tools to use for back-end proxy

For debugging locally, I am using `Node.js` and [http-server](https://github.com/http-party/http-server)

This console command, will run the `http-server` on current folder and will redirect all failed requests to main `index.html` and then JS router will take over.

`http-server -p 8080 . --proxy http://localhost:8080?`

For production, I am using [Caddy](https://caddyserver.com/docs/quick-starts#menu) as my backend proxy.
So here's a code-example how I send all 404 request to `index.html` in Caddy.

The `try_files` part, is where the failed routes are redirected.

```
https://www.mybook.com {
    root * /srv/www/mybook
    try_files {path} /index.html    
    encode zstd gzip
    file_server
}
```