Ive found Backbone to be one of the most useful client-side frameworks available due to it's lightweight nature. I know of a number of people that dislike it because it doesn't provide everything including the kitchen sink, but that's one of the reasons why I love it; it gives me the foundation to build exactly what I need, and only what I need.

Routing

One of the things that I find myself wanting when building a fairly large single page app is the ability to add middlewares to routes. Out of the box, Backbone's routing is extremely simple and looks something like this:

var app = Backbone.Router.extend({
    routes: {
        'users': function(){

        },
        'users/:id': function(id){
            // handle users/:id route
            console.log(id);
        }
    },
    initialize: function(){
        // initialize your app
    }
});

new app();
Backbone.history.start({ pushState: true });

For a lot of apps this is more than sufficient. But what if you want to add handlers that run before each route to do things like fetch data, or check to see if a user is authenticated and allowed to access that route.

Introducing Cortex

Cortex is a small library that allows you to set up chains of middlewares for each of your routes in the same way that Express does for your NodeJS server.

Let's take a look at simple example that does the same as our vaniall Backbone router above.

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.3.1/lodash.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js"></script>
<script type="text/javascript" src="<path>/cortex-x.x.x.min.js"></script>
<script type="text/javascript">
    $(function(){
        var cortex = new Cortex();

        cortex.route('users', function(route){
            // handle users route
        });

        cortex.route('users/:id', function(route){
            // handle users/:id route

            console.log(route.params.id);
        });

        var app = Backbone.Router.extend({
            routes: cortex.getRoutes(),
            initialize: function(){
                // initialize your app
            }
        });

        new app();
        Backbone.history.start({ pushState: true });
    });
</script>

This example should be pretty straightforward. Cortex.prototype.route takes at least two parameters:

  • A pattern to define the route. This is the exact same string we used in the vanilla Backbone example
  • A function to handle the route. This is the function that will be called when your route is matched. It takes two parameters:
    • route - This is an object that will contain things like url parameter tokens, query parameters, etc
    • next - This is a callback that can be called to move on to the next handler in the chain. In our example we dont call if because there is nothing after the handler we defined.

Lets add a middleware that will run before all routes:

$(function(){
    var cortex = new Cortex();

    cortex.use(function(route, next){
        // do something before all routes

        next();
    });

    cortex.route('users', function(route){
        // handle users route
    });

    cortex.route('users/:id', function(route){
        // handle users/:id route

        console.log(route.params.id);
    });

    var app = Backbone.Router.extend({
        routes: cortex.getRoutes(),
        initialize: function(){
            // initialize your app
        }
    });

    new app();
    Backbone.history.start({ pushState: true });
});

Middlewares function almost identically to those in Express save for the parameters that are passed (since we're not working with an HTTP server here). Middlewares will be called in the order they are defined. If you don't invoke the next callback, execution of the middleware/handler chain will stop at that point.

Now what if we want a chain of middlewares for a particular route:

$(function(){
    var cortex = new Cortex();

    cortex.route('users', function(route){
        // handle users route
    });

    var authUser = function(route, next){
        // check if the user is authenticated
        if(user.isAuthenticated){
            next();
        } else {
            throw new Error('User is not authenticated');
        }
    };

    cortex.route('users/:id', authUser, function(route){
        // handle users/:id route

        console.log(route.params.id);
    });

    var app = Backbone.Router.extend({
        routes: cortex.getRoutes(),
        initialize: function(){
            // initialize your app
        }
    });

    new app();
    Backbone.history.start({ pushState: true });
});

In this example, if the user is determined to be unauthenticated, we'll throw an exception. Cortex actually has a mechanism built in to handle exceptions that arise in middlewares/handlers. You can listen to the error event on your Cortex instance to handle errors:

$(function(){
    var cortex = new Cortex();

    cortex.on('error', function(err, route){
        // err - the error object/exception thrown
        // route - the route payload in the context the error was thrown
    });

    cortex.route('users', function(route){
        // handle users route
    });

    var authUser = function(route, next){
        // check if the user is authenticated
        if(!user.isAuthenticated){
            throw new Error('User is not authenticated');
        }
        next();
    };

    cortex.route('users/:id', authUser, function(route){
        // handle users/:id route

        console.log(route.params.id);
    });

    var app = Backbone.Router.extend({
        routes: cortex.getRoutes(),
        initialize: function(){
            // initialize your app
        }
    });

    new app();
    Backbone.history.start({ pushState: true });
});

In this error handler you can use the err object and route object to determine where the error happened and how to handle it.

The future

This is a very first iteration of this library, so expect that things will improve as time goes on. Future updates will include support for various module systems and possibly an Express middleware to make serving the individual file super easy.

Improvements and pull requests are more than welcome and can be created over at seanmcgary/backbone-cortex.