Chasten - Whither Conceptual Introduction

This is a conceptual introduction to whither and uses a lot of words on explanations. The alternative is the much terser API reference, which focuses on whither's public contract and might be more useful after you've learned the concepts.

Whither provides data-driven, bidirectional, purely functional, fast, sequence-independent, simple HTTP routing for JavaScript. That's quite the mouthful. In other and more voluminous words, whither is all of the below.

API Specification

An API specification is an object nested three levels deep. The first level has all known URL paths as keys, the second level has, for each path, all known HTTP methods as keys, and the third level has, for each method, an object with one mandatory key, id, and a number of optional ones.

Here's an example:

{
  "/users": {
    "GET": { "id": "get-all-users" },
    "POST": { "id": "create-user" }
  }
}

This is very resource-centric. First you define your resources by their path, then you define the methods they accept, and finally you flesh out some data that ascribes meaning and functionality to the endpoints.

Paths

The first level, called the path, must follow a few conventions for Whither to understand it. Each path is a string of slash-separated path segments, where a path segment is one of three things:

The first special string, "*", is called a segment wildcard. It will match any segment, that is, any sequence of characters that does not include a slash.

The second special string, "**", is called a path wildcard. It will match any number of segments including none, that is, any sequence of characters even the empty one or one including slashes. You can not have any additional segments after a path wildcard.

Any string that isn't one of the special strings will be considered a literal segment and will only match exactly those characters.

Examples

"/users/*/friends" will match "/users/12345/friends", but not "/users/friends" or "/users/12345/friends/23456".

"/pets/**" will match "/pets/1234/dogs/test" and "/pets", but not "/dogs/pets/123" or "/".

"/countries/dk" will match "/countries/dk" and nothing else.

Methods

The second level, called the method, must follow some conventions, too. Each method must be one of the following:

The special string, "*", is called a method wildcard. It will match any HTTP method.

Any string that isn't a method wildcard will only match exactly its own characters. You'll probably be using the defined HTTP methods (DELETE, HEAD, GET, PATCH, POST, PUT, etc), but Whither doesn't actually care. If you've modded Node.js to accept other methods, they'll work just fine with Whither.

Examples

"*" will match anything, like "DELETE", "GET", or even "Z̘̝̳͢al͙g͡o͖͍̭̤̖̤̥".

"PATCH" will match only "PATCH".

Endpoints

The third level, called the endpoint, is where you attach data to the destination of Whither's routing. Each endpoint must be an object. Anything you add to the object will be made available to the handler and middlewares that end up processing requests for the given path and method, and a few keys hold special meaning to Whither.

id is a mandatory key used in various ways by Whither. It must be unique, and ideally it should describe the endpoint in a way that makes sense to you. There are no other restrictions for this string.

HANDLER is an optional key that attaches a handler function to the endpoint. This key is mandatory if you're using Whither's handler function.

Router

A router is the result of compiling an API specification. It's the first argument for both byUrl and byName.

A router's primary purpose is to spend a bit of computation time up-front on constructing this data structure so that all runtime routing can happen efficiently.

Compiling is not difficult:

const { compile } = require('@chasten/whither');

const apiSpec = {
  '/users': {
    'GET': { 'id': 'get all users' },
    'POST': { 'id': 'create new user' },
  },
};

const router = compile(apiSpec);

It gives sensible error messages in case of issues with your API specification:

const { compile } = require('@chasten/whither');

const apiSpec = {
  '/users': {
    'GET': { 'id': 'get all users' },
    'POST': { 'id': 'create new user' },
  },
  '/users/*/friends': {
    'GET': {},
  },
};

const router = compile(apiSpec);
// Error: Route "GET /users/*/friends" is missing required key "id".