node package manager
Share your code. npm Orgs help your team discover, share, and reuse code. Create a free org »

camino

Camino

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

NPM

Support Development

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

Important changes

With IE 11 usage at a very low percentage (depending on who's data you believe) support will start phasing out to take advantage of es6 features. If you require IE11 support, use version 0.15.x.

v0.15.0

  • In an effort to reduce boilerplate code in browsers, an event listener was added (to window object) to listen for clicks on "A" tags. If the href attribute matches a route defined in camino, the request is routed in the following manner:
window.history.pushState( null, null, href );
window.dispatchEvent( new CustomEvent("popstate") );    
event.preventDefault();

This may not be the best way to handle it, but it works. Comments/pull requests welcome. This behavior can be turned off by setting the init option to false (see docs for setting options). This will also turn off initial firing of popstate event for routing on first page load.

v0.14.0+

  • Event firing was changed to use CustomEvent instead of Event constructor to support older version of IE.

  • Routes are now sorted using an alogirthm to try and match 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!

First things first, some juicy examples.

Usage:

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, UIMessageBoxOfSomeKind);

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

// 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>

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 );

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

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.


API

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

route --- String

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/

options --- Object

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)

callback --- Function

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

Server only:

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:

Server and Browser:

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

Browser only:

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().

Events

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

File Uploads

(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 );
});