camino

One-stop routing for server- and client-side web applications

Camino

One-stop routing for server- and client-side web applications

Bugs, suggestions, and pull requests welcome! Please submit to GitHub repository

Important changes

v0.14.0+

  • Routes are now sorted using an alogirthm to try and complete the most complete or explicit path first. For example, if your request URL is /api/user/data, and you have set up routes for /api/user/%user_id and /api/user/data, it will match /api/user/data first. This could affect how your existing routes are matched, so test carefully. Optionally, you can do (psuedo code) camino.listen([server||window], {sort: false}); to disable sorting.

  • Browser: In previous versions, if you attempted to load a request more than once (in a row) the subsequent requests were ignored. This has been changed so requests are always processed.

  • Browser: camino only listens to the popstate event, including for hash updates. The hashchange event is not longer monitored. This means only IE 9+ is supported.

  • If you see a bug in the documentation, please report it!

Camino is a request middle layer. It connects requests with callback functions and does not enforce any particular application paradigm. MVC, MVVM, or just write some closures to run some code, Camino doesn't care!

Support Development

First things first, some juicy examples.

Server

var camino = require("camino");

// just pretend user.init is part of some controller framework or something
camino.route( "/api/user/@id", { methods: ["POST"] }, user.init );

// org.init would be part of the same pretend controller framework
// passing in some sample options
camino.route( "/api/organization/@id", {
    responder: SomeCustomResponderObject,
    methods: ["GET", "POST"]
}, org.init );

// using the response object
var callback = function( request ) {
    var data = JSON.stringify({
        status: 200,
        success: true,
        data: request
    });

    // HTTP response object passed from node
    request.response.writeHead( 200, {
        "Content-Length": data.length,
        "Content-Type": "application/json"
    } );

    request.response.end(data);
};

// passing in a simple function as the callback for this route
camino.route( "/api/user/@user_id/message/%id", callback );

var http = require('http');
var server = http.createServer().listen(31415, '127.0.0.1');

camino.listen(server);

Browser (Hashes)

<script src="path/to/camino.js"></script>

camino.route( "#!/profile", user.init );
camino.route( "#!/team/@user_id", team.init );

// using "methods" in the browser
camino.route( "#!/message/%id", {
    responder: SomeMessageBoxObject,
    methods: ["read", "delete"]
}, message.init );

camino.listen(window);
OR
camino.listen(window, jQueryUIMessageBoxObjectWrapper);

// using the Camino.event object
window.addEventListener( camino.event.request, function() {
    // replace page body with a spinner...?
});

/**
 * This is no longer required, see History API example below for explanation

// fire a hashchange event for initial page loads
window.dispatchEvent( new Event("hashchange") );

 */

// browser methods, maybe...?
// psuedo jQuery code, really terrible example
$('[data-method]').on( "click", function() {
        // I don't remember if this is frowned upon... not sure if I care
        window.location.method = $(this).data('method');
});

// sample html for browser method
<a href="/#!/message/23" data-method="read">View Message</a>
<a href="/#!/message/23" data-method="delete">Delete Message</a>

Using the History API requires a bit more boilerplate code, but works quite nicely

Browser (History API, condensed example)

// include script and define routes
camino.route( "/profile", user.init );

camino.route( "#delete", DeleteConfirmationBox );

// History API is enabled by default
camino.listen( window );

/**
 * These two event firings are no longer required if the "init" option is
 * set to true (default). They are left here in the examples so you can see
 * what happens under the hood or so you can override them if desired.

// fire off route on initial page load
window.dispatchEvent( new Event('popstate') );

// hashes and history can live together!
if( window.location.hash !== '' )
    window.dispatchEvent( new Event('hashchange') );

 */

// using the Camino.event object
window.addEventListener( camino.event.request, function() {
    // replace page body with a spinner...?
});

// write your own vanilla JS, but here's the super-simple jQuery version
$( function() {
    $('body').on( {
        click: function(event) {
            window.history.pushState( null, null, this.getAttribute('href') );
            window.dispatchEvent( new Event("popstate") );

            event.preventDefault();
        }
    }, "a:not([href^=\\#])" );
});

Then put this in .htaccess

<ifModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule (.*) index.html [L]
</ifModule>

These browser examples are in no way exhaustive, nor "recommended practice". They abstractly illustrate usage. You can choose to support either or both.


camino.route( route [, options], callback )

The URL you are attempting to match. You can also capture "parameters" in your URL by using the @ symbol for a required param, or a % for an optional param. The difference between the two types is, if your URL has a required param but one is not provided, it will result in a (404 error).

/api/user/%id matches:

  • /api/user
  • /api/user/
  • /api/user/23

/api/user/@id matches:

  • /api/user/23

but not:

  • /api/user
  • /api/user/

Camino.route accepts 2 options:

methods is an array of supported methods for the URL. On the server, this will most likely be request methods. There is nothing built in to work in the browser, so you can devise your own trickery to use this however you want. If you don't pass in a list of allowed methods, any method will be accepted as long as a request matches a route.

responder is a custom function/library/whatever that you pass in to handle responses from your callbacks. If you provide a responder at the route level, it will be used instead of the global response object (if one even exists)

YOUR code that is run when a request is matched to a route. Your callback should accept 2 parameters: a request object, and a response object. The request object is either the http.request object (node.js) or the window.location object (browser), augmented with these additional properties (depending on the presence of matching data):

request.route --- an object of route specific data (that you provided) for the matched route. example:

{
    route: /api/user/@company/%id,

    callback: myCallback,

    // the "params" accepted by the route
    params: [
        "company",
        "id"
    ],

    responder: myResponseObject

    // the array of allowed methods, defaults to empty array for type consistency
    methods: [ ]
}

request.path --- the request string that was used for matching, without query string. In the browser, this will be either a URL or the hash fragment.

request.url --- same as path, but with full query string.

request.params --- an object, key-value pair of data extracted from the URLs.

request.query --- the query string received by the server, parsed into a JSON object.

request.qs --- the original query string. This property should also be added to window.location soon

request.request --- the native request object. Node.js: the http.IncomingMessage object, Browser: window.location

request.response --- the reponse object. Node.js: http.ServerResponse native object, Browser: whatever you set as the reponder, either globally or per-route

request.method --- a string, in the case of a server the request method. You could augment the window.location object with a "method" in the onclick event or something like that

request.raw --- the raw request body (if using built in content-type handlers)

request.data --- the request body after it has been parsed (if using built in content-type handlers).

The only built in content types are "application/x-www-form-urlencoded" and "application/json". If you overwrite these handler, request.raw and request.data will not be set.

In the browser it is possible to augment the location object with additional properties/data you want to pass around.


camino.listen( window [, options] [, responder] );

When you call the listen() function, you have the option of passing in a custom "response" object/function.

In the browser, this could be a data parser, a message box, or something like that.

On the server, it defaults to the HTTP response object, but feel free to be creative :) Define your own as a shortcut if you always respond with the same content type or something like that.

The reason for passing through the HTTP response object is to give the user total control over how requests are responded too. The intent of this library is to stay out of the way.

Valid options for camino.listen "options" argument are:

sort --- boolean, if true will sort routes for best possible URL resolution

decode --- decode query string data using decodeURI, defaults to true

history --- use history API for routing requests, defaults to true

hash --- listen to hashchange event and handle requests using URL hash, defaults to true

init --- boolean value, if true will fire initial events on page load; "popstate" for History API and "hashchange" for hashes. Leverages the "history" option to determine which ones to fire.


camino.handle( type, callback );

Registers a content type and it's corresponding handler. There are three content types defined by default: multipart/form-data, application/json, and application/x-www-form-urlencoded. These can easily be overridden with the handle function, as well as additional content types registered depending on your applications needs.

type --- String

callback --- Function


camino.location( location [, data] [, title ] );

Executes a request. This can be used instead of window.location to redirect to a different page. This is essentially just a wrapper for history.pushState() that also fires the popstate event to kick camino into action. Data and title are optional and correspond directly to the data and title arguments for history.pushState().


camino.replace( location [, data] [, title ] );

Works the same as camino.location(), but executes history.replaceState() instead of history.pushState().

For convenience, Camino.event container object exists with references to Camino's events. The following are Camino.event's properties:

request --- fired when a request is received by Camino

match --- fired when a route matching the request is found

exec --- fired when the callback is executed

error --- fired when an error is encountered

v0.12.0+

  • User callbacks are only supplied one object instead of two. The request object has been restructured internally. Some properties have new names. The new object contains the native request/response (along with convenience properties), rather than pollute those native objects. See the API section for updated documentation.

  • Server: Generic handling of file uploads has been removed, and dependency on busboy has also been removed. The code formerly used in the library has been moved to the polyfills section.

  • A default listener for error events is removed. Camino still emits an event, but no longer tries to handle it for you.

  • The History API is now enabled by default. It can be disabled by passsing {history: false} to camino.listen() (see docs)

(Bad) Exmaple of uploading files from a form

camino.handle('multipart/form-data', function( req ) {
    var self = this;
    var Busboy = require( 'busboy' );
    var busboy = new Busboy({ headers: req.request.headers });

    // container for uploaded files. it would be a much better idea to
    // use readstreams instead of buffers stored in memory. just an example
    req.files = {};

    // container for form fields
    req.data = {};

    // grab uploaded files and stream them into buffers
    busboy.on( 'file', function( field, file ) {
        var buf = [];

        file.on( 'data', function(data) {
            // push data chunks into contrainer
            buf.push( data );
        });

        file.on( 'end', function() {
            // when finished capturing data, Buffer it
            req.files[field] = Buffer.concat(buf);

            // blow chunks
            buf = undefined;
        });
    });

    // capture incoming fields as they are parsed
    busboy.on( 'field', function( field, val ) {
        req.data[field] = val;
    });

    busboy.on( 'finish', function() {
        self.emit( self.event.exec );

        // fire off route callback
        req.route.callback.call( null, req );

    });

    // cleansing power of the pipe!
    req.request.pipe( busboy );
});

CustomEvent

// thank you @https://github.com/jonathantneal/EventListener
// polyfill for using CustomEvent constructor in IE 9/10
! window.CustomEvent && (function() {
    window.CustomEvent = function CustomEvent( type, dict ) {
        dict = dict || {
            bubbles: false,
            cancelable: false,
            detail: undefined
        };

        try {
            var ev = document.createEvent('CustomEvent');
            ev.initCustomEvent(
                type,
                dict.bubbles,
                dict.cancelable,
                dict.detail
            );
        } catch( error ) {
            // for browsers which don't support CustomEvent at all,
            // we use a regular event instead
            var ev = document.createEvent('Event');
            ev.initEvent( type, dict.bubbles, dict.cancelable );
            ev.detail = dict.detail;
        }

        return ev;
    };
})();

Array.forEach

// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
if (!Array.prototype.forEach) {

    Array.prototype.forEach = function(callback, thisArg) {

        var T, k;

        if (this == null) {
            throw new TypeError(' this is null or not defined');
        }

        // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
        var O = Object(this);

        // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
        // 3. Let len be ToUint32(lenValue).
        var len = O.length >>> 0;

        // 4. If IsCallable(callback) is false, throw a TypeError exception.
        // See: http://es5.github.com/#x9.11
        if (typeof callback !== "function") {
            throw new TypeError(callback + ' is not a function');
        }

        // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
        if (arguments.length > 1) {
            T = thisArg;
        }

        // 6. Let k be 0
        k = 0;

        // 7. Repeat, while k < len
        while (k < len) {

            var kValue;

            // a. Let Pk be ToString(k).
            //   This is implicit for LHS operands of the in operator
            // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
            //   This step can be combined with c
            // c. If kPresent is true, then
            if (k in O) {

                // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
                kValue = O[k];

                // ii. Call the Call internal method of callback with T as the this value and
                // argument list containing kValue, k, and O.
                callback.call(T, kValue, k, O);
            }

            // d. Increase k by 1.
            k++;
        }
        // 8. return undefined
    };
}