How to implement dynamic routes in vanilla JavaScript?

If you’re building a Single Page Web Application with vanilla JavaScript, you probably will need to implement dynamic routes like “/project/edit/:id”. In this post, I will walk you through how to achieve that with vanilla JavaScript.

This is post is basically based on the conversation between me and AI, with some personal touch to help you — and me — better understand the logic behind dynamic routes. Enjoy!

If you’re completely new to client side routing, this blog series might a good start for you: https://jessedit.netlify.app/blog/client-side-routing

We’ll start with this router class to get the basic features up and running. And we import this router class into our index.js and then create a new router instance along with some routes for our application. As you can see here, we have a dynamic routes /project/edit/:id, this is where we would need to code and grab the dynamic part id from given url.

index.js
import Router from "./router.js";

const ROUTER = new Router([
  {
    path: '/',
    component: 'home-page' // custom element
  },
  {
    path: '/dashboard',
    component: 'account-dashboard' // custom element
  },
  {
    path: '/project/edit/:id',
    component: 'edit-project' // custom element
  }
]);
router.js
export default class Router {
  constructor(routes) {
    this.routes = routes;
    this.navigate(window.location.pathname);
  }

  navigate(url) {
    // Trying to match the url user visits to a route that we define
    // when we create a new router
    const route = this.#matchUrlToRoute(url);
  }

  #matchUrlToRoute(url) {
    // params will be storing some information of matched route
    const params = {};

    // If url ends with "/", e.g. "/project/edit/123/",
    // then remove the trailing slash using replace() method with a regular expression.
    if (url.endsWith('/')) {
      url = url.replace(/\/$/, '');
    }

    // When we visit url: /project/edit/123,
    // first we need to figure out which route match the url pattern.
    const matchedRoute = this.routes.find((route) => {
      // '/dashboard' will not match '/project/edit/123'
      // as they have different length if we compare them after split with "/".
      if (url.split('/').length !== route.split('/').length) {
        return false;
      }

      // '/project/edit/:id' => [ "project", "edit", ":id" ]
      let routeSegments = route.split('/').slice(1);
      // '/project/edit/123' => [ "project", "edit", "123" ]
      let urlSegments = url.split('/').slice(1);

      // If each segment in the url matches the corresponding segment in the route path,
      // or the route path segment starts with a ':' then the route is matched.
      const match = routeSegments.every((segment, i) => {
        return segment === urlSegments[i] || segment.startsWith(':');
      });

      // If the route matches the URL, pull out any params from the URL.
      if (match) {
        routeSegments.forEach((segment, i) => {
          if (segmeng[0] === ':') {
            const propName = segment.slice(1);
            params[propName] = decodeURIComponent(urlSegments[i]);
          }
        });
      }

      return match;
    });

    return { ...matchedRouter, params };
  }
}