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.
- Bidirectional routing means that whither can either find the destination of a request given an HTTP method and a path or it can construct the destination based on a destination name. One direction is useful when you need to find the right handler for an incoming request, and the other is useful when you need to construct URLs to other API endpoints, e.g. when creating cross-resource links.
- Data-driven routing means that API specifications are expressed as data as opposed to brought to a certain state through method calls. Such an API specification is then compiled by Whither to produce an efficient routing structure called a router. The bidirectional routing works on this compiled router.
- Fast is surely a hard fact backed up by scientific benchmarks in a not too distant future.
- Purely functional refers to the ability to use Whither without ever leaving the comfort of referentially transparent, side effect-free functions and data. Absolutely nothing changes state or behavior by calling Whither's functions. You merely get data or functions returned that you need to plug into Node.js' request handling flow yourself.
- Sequence-independant means that the order in which API specifications are defined in your data affects neither the matching nor the performance of the matching done by Whither. You can use whichever order that makes sense to you.
- Simple is to be understood in the sense of "kept separate" where routing is not intertwined with concerns such as request processing or network traffic. Whither only concerns itself with finding what's called a destination, and can do this completely independently from any request processing, really. You could in theory use Whither without intending to answer HTTP requests at all. However, most often you'll combine a router with Whither's ability to turn a router into a Parfay handler by using Whither's handler function
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 special string
"*"
. - The special string
"**"
. - Any other string without slashes in it.
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
"*"
. - Any other string.
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".