K-Router

© Keith Cirkel

K-Router may be freely distributed under the MIT license.

For all details and documentation: http://github.com/keithamus/k-router

K-Router is a simple Class based router Connect middleware. In other words, it ehances the default Connect router (app.use('/url', fn)) to do more advanced routing to methods on instance object by router.route('/url', controller, 'action', 'GET'). The URLs work mostly like Expresses routing engine, it supports named and optional parameters, regex urls and can be bound to one HTTP verb at a time. It goes further than Expresses router in the following ways though:

Below is the heavily commented codebase for K-Router, which you can use for documentation. Failing that, you can also have a look at the test generated documentation

K-Router is a singleton, and has a global config. The config comes with a complete set of sensible defaults, so no configuration needs to be used.

var parseUrl = require('url').parse;
var config = {
    resourceActions: {
        list: 'list',
        new: 'new',
        create: 'create',
        show: 'show',
        edit: 'edit',
        update: 'update',
        destroy: 'destroy',
    },
    methods: {
        GET: true,
        HEAD: true,
        POST: true,
        PUT: true,
        DELETE: true,
        TRACE: true,
        OPTIONS: true,
        CONNECT: true,
        PATCH: true
    },

errorHandlers

    errorHandler: function (req, res, next) {
        var verb = req.method,
            err = new Error('Method ' + verb + ' not defined on route');
        err.allow = [];
        for (var httpVerb in config.methods) {
            if (route.routes[httpVerb][req.route.regexp.source].controller !== config) {
                (err.allow || (err.allow = [])).push(httpVerb);
            }
        }
        res.setHeader('Allow', err.allow.join(', '));
        res.statusCode = err.status = 405;
        return next(err);
    }
};

Helpers

pathRegExp is a way to convert string paths (first argument of route/resource) into RegExps. It is totally stolen from Express to remain compatible with Expresses matchers.

function pathRegExp(path, keys) {
    if (path instanceof RegExp) return path;

Multiple string paths can be expressed as arrays

    if (Array.isArray(path)) path = '(' + path.join('|') + ')';
    path = path

If "strict" is false (default) then allow for trailing /

        .concat(config.strict ? '' : '/?')

So that match groups aren't messed up, turn any /( into (?:/

        .replace(/\/\(/g, '(?:/')

This is the core of named groups, fragments like /:name get converted to /([^/]+?) and the key of "name" is recorded into keys so it can be loaded. It also has special cases for extensions in urls (i.e "/whatever.:format") and wildcards like "/whatever/*"

        .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?(\*)?/g, function (_, slash, format, key, capture, optional, star) {
            keys.push({ name: key, optional: !! optional });
            slash = slash || '';
            return '' +
                (optional ? '' : slash) +
                '(?:' +
                (optional ? slash : '') +
                (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' +
                (optional || '') +
                (star ? '(/*)?' : '');
        })

Escape all /s and .s as RegExp literal /s and .s

        .replace(/([\/.])/g, '\\$1')

Convert any wildcards to RegExp wildcards

        .replace(/\*/g, '(.*)');

If "sensitive" is false then make the RegExp case insensitive (//i)

    return new RegExp('^' + path + '$', config.sensitive ? '' : 'i');
}

Routing

Route

Route allows you to bind a path to an controller.action against an HTTP verb, for example route('/user/:name', userController, 'show', 'GET');

function route(path, controller, action, verb) {

Clean up the verb name. Throw if it doesn't exist

    verb = verb ? verb.toUpperCase() : 'GET';
    if (!config.methods[verb]) throw new Error('Invalid HTTP method: ' + verb);

Build up the RegExp pathname (see above)

    var keys = [];
    var regexp = pathRegExp(path, keys);

Bind all routes to each of the HTTP verbs, attaching 405s to the other verbs not specified in the, params if they don't exist.

    for (var httpverb in config.methods) {

If a method is already attached (and its not the one we're explicitly trying to set), then don't add one, otherwise just go ahead and add one in.

        if (httpverb !== verb && (route.routes[httpverb] || {})[regexp.source]) continue;
        var routeObj = (route.routes[httpverb] || (route.routes[httpverb] = {}))[regexp.source] = {
            path: path,
            regexp: regexp,
            keys: keys
        };

Attach the controller & action to the HTTP verb supplied, but attach a 405 handler for any other verb.

        routeObj.controller = httpverb === verb ? controller : config;
        routeObj.action = httpverb === verb ? action : 'errorHandler';
    }

Return the function so multiple routes can be chained like: `route(/.../)(/.../)(/.../)(/.../)

    return route;
}

This allows users to use the syntax "route(/.../).route(/.../).route(/.../).route(/.../)" for people not adventurous to use the paren only syntax ( (/.../)(/.../) )

route.route = route;

Resources

Resources are a shortcut to declaring many routes. They follow the CRUD pattern, and are similar to how Rail's resources work. Resources come in two flavours, .resource() and .resources(). The plural .resources() binds the list, create, new, show, update, destroy, edit actions, while the singular .resource() binds the show, create, update, destroy, new, edit actions. If one of those actions doesn't exist on the controller, then the route isn't bound. See the handy table below for how these get mapped to different urls:

Resources (Plural, .resources())

Verb Url Controller Action Description
GET /url Controller#list Display a list of all items in a resource
POST /url Controller#create Create a new item
GET /url/new Controller#new HTML form for creating new items
GET /url/:id Controller#show Display a specific item
PUT /url/:id Controller#update Update a specific item
PATCH /url/:id Controller#update Update a specific item
DELETE /url/:id Controller#destroy Delete a specific item
GET /url/:id/edit Controller#edit HTML form for editing an item
route.resources = function resource(url, controller, id) {
    id = id || ':id';
    var urlid = url + '/' + id, actions = config.resourceActions;
    if (controller[actions.list])    route(url, controller, actions.list, 'GET');
    if (controller[actions.create])  route(url, controller, actions.create, 'POST');
    if (controller[actions.new])     route(url + '/new', controller, actions.new, 'GET');
    if (controller[actions.show])    route(urlid, controller, actions.show, 'GET');
    if (controller[actions.update])  route(urlid, controller, actions.update, 'PUT');
    if (controller[actions.update])  route(urlid, controller, actions.update, 'PATCH');
    if (controller[actions.destroy]) route(urlid, controller, actions.destroy, 'DELETE');
    if (controller[actions.edit])    route(urlid + '/edit', controller, actions.edit, 'GET');
    return route;
};

Resource (Singular, .resource())

Verb Url Controller Action Description
GET /url Controller#show Display the singular resource
POST /url Controller#create Create a new resource like this
PUT /url Controller#update Update a specific item
PATCH /url Controller#update Update a specific item
DELETE /url Controller#destroy Delete a specific item
GET /url/new Controller#new HTML form for creating new items
GET /url/edit Controller#edit HTML form for editing an item
route.resource = function resource(url, controller, id) {
    id = id || ':id';
    var actions = config.resourceActions;
    if (controller[actions.show])    route(url, controller, actions.show, 'GET');
    if (controller[actions.create])  route(url, controller, actions.create, 'POST');
    if (controller[actions.update])  route(url, controller, actions.update, 'PUT');
    if (controller[actions.update])  route(url, controller, actions.update, 'PATCH');
    if (controller[actions.destroy]) route(url, controller, actions.destroy, 'DELETE');
    if (controller[actions.new])     route(url + '/new', controller, actions.new, 'GET');
    if (controller[actions.edit])    route(url + '/edit', controller, actions.edit, 'GET');
    return route;
};

Notes:

MiddleWare

The dispatcher is the main component exposed in the middleware for Connect. It sits (ideally at the top) in middleware stack and waits for incomming requests, where it tries to match them up to a route. Failing that it hands of to the next() middleware.

function dispatcher(req, res, next) {
    var verb = req.method,

Usi

        url = parseUrl(req.url).pathname;

Because all of the routes are bound to the HTTP verb first, the array of acceptable routes can be shrunk down significantly by using the HTTP verb.

    for (var name in route.routes[verb]) {
        var arg = route.routes[verb][name].regexp.exec(url);

Arg is either null (no match) or an array of atleast 1 (match)

        if (arg) {

The params get injected into the route object, which gets injected into the req object, just like Express (so req.route.params works as well as req.params)

            req.route = route.routes[verb][name];
            req.params = req.route.params = [];

Build up the params list out from the RegExp and the keys of named parameters.

            for (var key, value, i = 1, l = arg.length; i < l; ++i) {

The match of arg[i] will either be a string or undefined. If it is a string, decode it so rather than "foo%20bar" its "foo bar".

                value = arg[i] === undefined ? arg[i] : decodeURIComponent(arg[i]);

If it is a named parameter, add it to the array with its name, so it can be used as req.params.name. If not, then push so it is req.params[0]

                if (key = req.route.keys[i - 1]) {
                    req.params[key.name] = value;
                } else {
                    req.params.push(value);
                }
            }

Dispatch the route, passing on all params.

            return req.route.controller[req.route.action](req, res, next);
        }
    }

    next();
}

setupRoute organises the initial configuration and returns the dispatcher, exposing the dispatcher to the app using the typical paradigm app.use(setupRoute({ /*config*/ })); If the user attempts to reconfigure the router by running setupRoute again, it will flush any existing routes.

module.exports = function setupRoute(options) {
    options = options || {};
    config.sensitive = Boolean(options.urlsCaseSensitive);
    config.strict = Boolean(options.urlsAllowTrailingSlashes);
    config.errorHandler = options.urlMethodNotAllowedErrorHandler || config.errorHandler;
    for (var key in options.urlResourceActions || {}) {
        config.resourceActions[key] = options.urlResourceActions[key] || config.resourceActions[key];
    }
    route.routes = [];
    return dispatcher;
};

Expose the routing functions to the user.

module.exports.route = route;
module.exports.resource = route.resource;
module.exports.resources = route.resources;
h