Routing

The whole site runs inside one server callback. Routes are declared with WebRoute and matched over two passes: a scan pass records named routes so links can be built by name, then a render pass matches the request URL and renders the page.

The server callback

The entry point ends by calling NetworkWeb::Server with a single callback. Everything the site serves happens inside that callback. The server reads the request URL, splits it into path segments, and runs the callback:

NetworkWeb::Server(function() {

    // declare and match routes here

});

Declaring routes with WebRoute

Routes are declared with WebRoute. WebRoute::Dir descends into a path segment, WebRoute::File matches all the way to the end of the path and renders, and WebRoute::String captures a segment as a value. A named anchor is declared with WebRoute::Named, which lets a link be built later by name instead of by hardcoding a path.

Routes are matched first come first served. The first route that matches wins, so specific routes are declared before any catch-all. A bare /<permalink> route that matches almost anything is declared last so it never shadows the named routes above it.

Two passes

The same route definitions run in two modes. The scan pass walks every route with WebRoute::Scan and records each WebRoute::Named anchor. It renders nothing: terminals and captured parameters do nothing during a scan. After the scan, every named link can be resolved with WebRoute::GetNamed, so a page rendered later can link to a route by name.

The render pass then matches the request URL against the routes and renders the one page that is hit. Because the scan already recorded every name, links on that page resolve even to routes that were never visited by this request.

A small example

Modelled on the real entry point: a scan pass records the named routes, then a render pass serves the public site, the member front end, the API, the admin, and last of all the catch-all permalink route:

NetworkWeb::Server(function() {

    // Pass 1 (scan): record named routes so links resolve by name.
    WebRoute::Scan(function() {
        ClipMember::Route();
        ClipSite::RoutePermalink();
    });

    // Pass 2 (render): match the request URL and render the page.
    ClipSite::Route();
    ClipMember::Route();
    Api::Route();
    Admin::Route('/admin');

    // LAST: bare "/<permalink>" is registered after every named route,
    // so first-match-wins never lets it shadow them.
    ClipSite::RoutePermalink();
});

Each Route method is a module declaring its own routes with WebRoute. The site composes them by order: the more specific a route, the earlier it is declared.

Routing is declarative

Routing is a good example of declarative code. A module does not inspect the request and hand-match it; it declares the routes it owns with WebRoute and leaves the matching to the framework. The site composes those declarations by order and the two passes work out the rest. You state which routes exist, not how each request is dispatched. See Declarative code for the same idea applied to building pages.

Routing catches exceptions at the top

The render pass is another top-level place where exceptions are caught, which keeps page code to a single happy path. A page assumes the conditions it needs and throws when they are not met, rather than guarding every step. The common case is a member page that requires a login: it throws MemberNotLoggedInError, and the top of the render pass catches it and redirects to the login page with Ui::OverrideRedirect(MemberUrl::Login()).

Because that catch lives at the top, the page itself never checks whether a member is logged in before doing its work. It writes what the page should show for a logged-in member and lets the throw handle the exception, in the same spirit as the Api handlers described in The Api layer.

Next: The Api layer.