npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

that-routing-lib

v1.3.1

Published

A simple api for your routing needs

Downloads

12

Readme

that-routing-lib

Stop worrying about those URLs

Intro

This library provides an easy-to-use API to build URLs in your JS/TS-project. Creating a URL-string that links to a given location becomes as simple as

// "/articles/4/edit"
const editArticle4 = routesApi.articles.$articleId("4").edit();

with full Typescript support.

that-routing-lib has been designed with the Angular router in mind, but the client side functionality can be used to construct URLs for any setting.

Why use this lib?

Whenever we need a URL in our projects, we can typically choose to either

  • Write the URL out as a string
  • Use predefined constants for given URLs

The first method is prone to typos and difficult to adjust should things change eventually. The latter method is safe with regard to these problems, but poses the question of which parts of a URL to save as constants. The entire URL? Or each segment separately? Saving the entire URL, as in /topics/cooking/soup is easy to use, but requires a lot of variables for each possible URL, such as /topics/cooking/fish and /topics/travel/spain. When saving each segment, we avoid having one constant per leaf in our routes tree, but have no way of knowing whether TOPICS + '/' + FISH + '/' + SPAIN is a valid URL or not. And what about route parameters as in /topics/travel/articles/4?

We need a scheme that allows us to

  • centrally define all routes to avoid typos
  • preserve the structure of route segments and URL params
  • easily refactor code
  • access URLs easily with minimal risk of mistakes

that-routing-lib helps you in fulfilling all these requirements, by allowing you to specify your routes with minimal effort in a tree-like structure and providing functions to turn this data into function objects with a super nifty developer experience.

How to define your URL tree

In the simplest case, your URLs are defined as follows

import {buildRoutes} from './that-routing-lib';

const routesDefinition = {
    home: {},
    topics: {
        subRoutes: {
            cooking: {
                subRoutes: {
                    soup: {},
                    fish: {}
                }
            },
            travel: {
                subRoutes: {
                    spain: {},
                    articles: {
                        subRoutes: {
                            $articleId: {}
                        }
                    }
                }
            }
        }
    }
};

const routesApi = buildRoutes(routesDefinition);

The strings of the keys which you use are directly turned into the strings of your URL segments. URL params are prefixed with a dollar sign $, which will make the resulting API prompt for a string parameter when you construct an actual instance of the URL.

Reserved keywords

Under the hood, the library constructs a nested function object. That's why, at any point along your route, you can either call the function to return the string, or continue to build a URL that is nested more deeply:

// "topics/travel"
const routeToTravel = routesApi.topics.travel();

// "topics/travel/acticles/5
const routeToTravelArticle = routesApi.topics.travel.articles.$articleId("5")();

Unfortunately, with any segment of the API being a function, there are a few reserved keywords which can not be used, because they are either readonly (such as a function's name) or should not be overwritten (like bind). When you try to create the API object with input that contains these keys, an error will be thrown.

Reserved keywords are

['name', 'arguments', 'length', 'caller', 'prototype', 'bind', 'call', 'apply', 'constructor', 'hasOwnProperty', 'isPrototypeOf', 'length', 'toString', 'propertyIsEnumerable', 'toLocaleString', 'valueOf']

Since it is quite likely that strings like name will be part of a URL, you can overwrite the string associated with a given URL segment with the segmentName property:

import {buildRoutes} from './that-routing-lib';

const routesDefiniton = {
    user: {
        subRoutes: {
            $userId: {
                subRoutes: {
                    uname: {
                        segmentName: "name" // overwrite
                    }
                }
            }
        }
    }
};

const routesApi = buildRoutes(routesDefiniton);

// "user/78357/name"
const nameUrl = routesApi.user.$userId("78357").uname();

this can also be useful to prepend slashes, entire domain names, or insert chars that would require string notation for keys when used directly:

import {buildRoutes} from './that-routing-lib';

const routesDefiniton = {
    sameOrigin: {
        segmentName: "", // leads to a leading '/'
        subRoutes: {
            home: {},
            articles: {}
        }
    },
    analytics: {
        segmentName: "https://my-analytics.com",
        subRoutes: {
            pageEnter: {
                // avoid routesApi.analytics['page-enter'].$pageId("3")()
                segmentName: "page-enter",
                subRoutes: {
                    $pageId: {}
                }
            },
            pageNavigation: {
                segmentName: "page-navigation",
                subRoutes: {
                    $fromPageId: {
                        subRoutes: {
                            $toPageId: {}
                        }
                    }
                }
            }
        }
    }
};

const routesApi = buildRoutes(routesDefiniton);

// "https://my-analytics.com/page-navigation/etkceaua/aetaeo"
const navigationAnalyticsUrl = routesApi.analytics.pageNavigation.$fromPageId("etkceaua").$toPageId("aetaeo")();

Note: You can not use segmentName to overwrite $parameters, because no reserved keyword starts with a $, and there is no other reason why you'd want to do this for parameters.

Creating URL strings for the Angular router

When you use routing in Angular, you have to define which component is to be shown under which route. Of course, you'll want to have a single source of truth, so your routesInput must be able to produce those route definitions as well.

Two requirements have to be fulfilled for this to work:

  • Route params have to be printed with the colon syntax acticles/:articleId
  • Routes can be nested. Instead of parentRoute/childRoute you'll have to be able to produce parentRoute and childRoute separately

The router API works exactly the same as the client API, except that now you don't have to pass a string to parameter segments. To signal that a given route is a parent route, you add the key isParent: true to the object:

import {buildRoutesForAngularRouter} from './that-routing-lib';

const routesDefinition = {
    topics: {
        subRoutes: {
            travel: {
                isParent: true,
                subRoutes: {
                    articles: {},
                    spain: {}
                }
            },
            otherParent: {
                isParent: true,
                subRoutes: {
                    $parameter: {}
                }
            }
        }
    }
};

const routesApi = buildRoutesForAngularRouter(routesDefinition);

// "topics/travel
const urlForParentForRouter = routesApi.topics.travel();
// "articles"
const urlForChildForRouter = routesApi.topics.travel.articles();
// ":parameter"
const urlForParameterChildForRouter = routesApi.topics.otherParent.$parameter();

Extract parameters to get data from router

We need the name of the parameter to obtain data from the activated route that has been inserted in the URL

const articleId = this.activatedRoute.snapshot.params['articleId'];

This is another case which is prone to errors. How do we know which parameter names exist? We'd have to check in the route definitions, and we'd have no way to safely refactor this code.

But we can do this:

import {getParameterExtractor} from './that-routing-lib';

// In central location:
export const parameters = getParameterExtractor().extract(routesDefinition);

// In component
const articleId = this.activatedRoute.snapshot.params[parameters.$articleId];

This isn't perfect. We have no way of knowing whether $articleId actually is part of the route behind which our component sits (this is always the case when using the router). In fact, we don't know at all which routes contain this parameter. But we do know that some route contains this parameter, which gives some confidence that what we do is right. And there is no way we produce a typo this way.

Gotchas

  • Due to Typescript technicalities, the maximum depth of your URLs is 30ish segments. That's because a recursive type has to be used, and the stack-size that TS allows is very limited. It's possible that this limit will be increased eventually.
  • On TS versions below 4.5, recursive types are not yet optimized if they are tail recursive. Since this library depends on a recursive type, the provided default of 95 for the maximum recursion depth (leading to those 30ish segments) is too large, leading to TS2589: Type instantiation is excessively deep and possibly infinite errors. If you use the getParameterExtractor().extract(routesDefinition) function on these versions, you need to limit the recursion depth to 16 to avoid the error: getParameterExtractor<16>().extract(routesDefinition). That also means that the depth of the URLs for which TS will provide support will shrink considerably.

Open issues

  • No support for query params