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

@davidmz/just-router

v0.3.1

Published

A very simple path matching and routing library

Downloads

8

Readme

just-router

A very simple path matching and routing library. It is framework-independent, works synchronously, has a small size (< 750 B min/gz) and concise API.

Installation

Install it from npm as @davidmz/just-router

Usage (sort of tutorial)

What is "router" and how to create one

Routers are created by createRouter(…) call.

The router itself has a simple function <T>(path: string) => T. It takes path (string) and returns something that makes sense for your application. It can be, fo example, a React component or HTML string.

Path is a regular pathname, the string like "/path/to/some/thing". Path actually processed as a list of segments: "/path/to/some/thing" -> ["path", "to", "some", "thing"]. All slashes are ignored: "/foo", "/foo/", "foo" or "///foo/" are all the same for the router.

Handlers (who do everything here)

The createRouter function takes single argument of type Handler. Handler is the main working type for the all library. It is a function with signature:

type Handler<T, S extends object = object> = (
  context: Context<S>,
  next: Next<Nullable<T>>
) => Nullable<T>;

It may look complex because of type declarations, but actually it is very simple middleware-like (as in koa or other routing libs) function that accepts the request context and the next function, and returns type T or nothing (null | undefined). If handler is a middleware (i.e. it just prepare data for the next handlers), it should call next. If it is a terminal handler, it shouldn't (this will cause an error).

By the way, the context has the following type:

type Context<S extends object = object> = {
  // The initial 'path' passed to router. Immutable.
  readonly path: string;
  // The path segments. This list can be altered by handlers.
  segments: string[];
  // Named parameters, taken from path segments (see _param_ handler).
  pathParams: Record<string, string>;
  // An arbitrary request state. Handlers can store everything in it.
  state: S;
};

The simplest router looks like this:

const router = createRouter(() => "Hello!");

expect(router("/foo/bar")).toBe("Hello!");

This router is pretty useless because it will return "Hello!" for any given path. The createRouter hasn't any path processing logic, we need an additional handlers for it.

Routes and path matching

The route function creates handler that allows to check path segments and call the terminal handler (handler that returns value and don't call 'next'):

const router = createRouter(route("foo", () => "Hello!"));

expect(router("/foo")).toBe("Hello!");
expect(() => router("/foo/bar")).toThrow(RouteNotFound);

route takes variable (but at least one) number of arguments. The last argument must be a handler function, other can be handler functions, strings, or regexps.

route internally converts strings and regexps to the regular handlers. String means that the segment must exactly match the string. Regexp also requires match, but can also capture part of the segment (see below).

The route arguments forms handlers chain and executes sequentially, being linked via the next argument of each other. So route(a, b, c) will act like a(ctx, () => b(ctx, () => c(ctx, next))).

Path parameters

Having route and middleware handlers, we can now process path parameters. Like this:

const router = createRouter(
  route(
    "articles",
    param("slug"),
    (ctx) => "Article: " + ctx.pathParams["slug"]
  )
);

expect(router("/articles/routing")).toBe("Article: routing");

The param helper takes current path segment and places it to the context's pathParams object with the given key.

Regexp matchers

Regexp matchers combines string and param functionality, since it allows you to both specify a segment pattern and capture come data from it.

To match specific segment format just use a relevant regexp: the /[1-9]\d*/ will match only the numeric segments.

To capture segment or it part(s), use regex named groups:

const router = createRouter(
  route(/article(?<id>[1-9]\d*)/, (ctx) => "Article ID=" + ctx.pathParams["id"])
);

expect(router("/article42")).toBe("Article ID=42");

'split' helper

Route arguments work on a per-segment basis, but sometimes it is more convenient to specify (sub)path as a single string. It is possible with split helper. The following routes are equivalent:

route("path", "to", "articles", param("id"), showArticle);
route(split("path/to/articles"), param("id"), showArticle);

Root path

The root path can be matched by the route with single terminal handler:

const router = createRouter(route(() => "I am root!"));

expect(router("/")).toBe("I am root!");

Bunch of routes

So far we have dealt with one route, which matches a single path to a single terminal handler. In practice we need to handle many different paths, a bunch of them. That's what the bunch function is for.

const router = createRouter(
  bunch(
    route(() => "Root page"),
    route("foo", () => "Foo page"),
    route("bar", () => "Bar page")
  )
);

expect(router("/")).toBe("Root page");
expect(router("/foo")).toBe("Foo page");
expect(router("/bar")).toBe("Bar page");
expect(() => router("/baz")).toThrow(RouteNotFound);

The bunch function takes set of handlers and returns a handler (again!) that:

  • Calls given handlers from first to last;
  • Stops on first non-nullish result and returns it.

Catch-all handlers

You may notice that our router throws a RouteNotFound error it there is not matched route. This is because handlers, created with route, (almost) always require a complete path match. In practice, we often need a handler for "all other" paths, for example to show the "Not found" page.

Fortunately, it's very easy to add it. Remember that the "bare" handler doesn't care about path matching. So we can do the following:

const router = createRouter(
  bunch(
    route("foo", () => "Foo page"),
    () => "Not found"
  )
);

expect(router("/foo")).toBe("Foo page");
expect(router("/bar")).toBe("Not found");

Greedy handlers

As was mentioned earlier, route requires the complete path match. This means that at the time the last handler in the chain is called, all segments of the path must be processed. So the route("foo", handler) will match "/foo", but not "/foo/bar", although the last one also starts from "foo" segment.

Sometimes we need a "greedy" handler that grabs all the remaining segments. In such case, just wrap you handler with batch. The handler the bunch returns is greedy, this allows to make nested bunches.

const router = createRouter(
  bunch(
    route("foo", () => "Foo page"),
    route(
      "bar",
      bunch((ctx) => "Bar page: " + (ctx.segments.join(", ") || "-"))
    )
  )
);

expect(router("/foo")).toBe("Foo page");
expect(router("/bar")).toBe("Bar page: -");
expect(router("/bar/baz")).toBe("Bar page: baz");
expect(router("/bar/baz/qux")).toBe("Bar page: baz, qux");

Nested bunches

Here we can create a really complex staff!

const router = createRouter(
  bunch(
    route(() => "Home"),
    route("about", () => "About"),
    route(split("about/contacts"), () => "Contacts"),
    route(
      "projects",
      bunch(
        route(() => "Projects list"),
        route(param("name"), (ctx) => "Project: " + ctx.pathParams["name"]),
      )
    ),
    route(
      "admin",
      checkRights, // Your own logic!
      bunch(
        route(() => "Admin home"),
        route("users", () => "List of users")
        () => "Admin page not found"
      )
    ),
    () => "Page not found"
  )
);

expect(router("/")).toBe("Home");
expect(router("/about")).toBe("About");
expect(router("/about/contacts")).toBe("Contacts");
expect(router("/projects")).toBe("Projects list");
expect(router("/projects/foo")).toBe("Project: foo");
expect(router("/admin/foo")).toBe("Admin page not found");
// ...and so on

That's all you need to know!

API methods signatures

createRouter

function createRouter<T, S>(route: Handler<T, S>): Router<T>;

route

function route<T, S>(
  ...handlers: [...(string | Handler<T, S>)[], Handler<T, S>]
): Handler<T, S>;

bunch

function bunch<T, S>(...handlers: Handler<T, S>[]): Handler<T, S>;

param

function param<T, S>(name: string): Handler<T, S>;

split

function split<T, S>(path: string): Handler<T, S>;