RequireJS is a JavaScript module loader which answers the question "how do I organize my JavaScript code?". I've recently come to appreciate the benefits of RequireJS with highly structured frameworks like Backbone.js. Without it, JavaScript becomes lengthy and disorganized, specific functionality becomes difficult to locate, and dependencies become time consuming to trace. While there is some upfront effort in moving your project to RequireJS, it will make development much more efficient in the long run. This article will explain an easy simplification that makes routing with RequireJS much easier.

Starting out, I followed the file structure and guidelines described in the Backbone Tutorial, "Organizing your application using Modules (require.js)". Over time, our application router became bulky, repetitive, and error-prone. Fortunately, there was an obvious pattern. A route, or URL pattern, directly corresponds to a View, which is constructed and rendered. Shouldn't that be a single line? I thought so. To demonstrate this for you, I've modified the router in Backbone Boilerplate, a great starting point for modularized Backbone.js applications. If you have git installed, pull down the code.

git clone https://github.com/thomasdavis/backboneboilerplate.git

If you open backboneboilerplate/js/router.js, it should look like this:

// Filename: router.js
define([
 'jquery',
 'underscore',
 'backbone',
 'vm'
], function ($, _, Backbone, Vm) {
 var AppRouter = Backbone.Router.extend({
 routes: {
 // Pages
 'modules': 'modules',
 'optimize': 'optimize',
 'backbone/:section': 'backbone',
 'backbone': 'backbone',
 'manager': 'manager',
 
 // Default - catch all
 '*actions': 'defaultAction'
 }
 });
 
 var initialize = function(options){
 var appView = options.appView;
 var router = new AppRouter(options);
 router.on('route:optimize', function () {
 require(['views/optimize/page'], function (OptimizePage) {
 var optimizePage = Vm.create(appView, 'OptimizePage', OptimizePage);
 optimizePage.render();
 });
 });
 router.on('route:defaultAction', function (actions) {
 require(['views/dashboard/page'], function (DashboardPage) {
 var dashboardPage = Vm.create(appView, 'DashboardPage', DashboardPage);
 dashboardPage.render();
 });
 });
 router.on('route:modules', function () {
 require(['views/modules/page'], function (ModulePage) {
 var modulePage = Vm.create(appView, 'ModulesPage', ModulePage);
 modulePage.render();
 });
 });
 router.on('route:backbone', function (section) {
 require(['views/backbone/page'], function (BackbonePage) {
 var backbonePage = Vm.create(appView, 'BackbonePage', BackbonePage, {section: section});
 backbonePage.render();
 });
 });
 router.on('route:manager', function () {
 require(['views/manager/page'], function (ManagerPage) {
 var managerPage = Vm.create(appView, 'ManagerPage', ManagerPage);
 managerPage.render();
 });
 });
 Backbone.history.start();
 };
 return {
 initialize: initialize
 };
});

Notice that in order to set up a single route, the developer adds an entry to the "routes" option of AppRouter, then creates an event handler (on method) for when the route is triggered. In total, there are 7 lines per route that exhibit the "copy-and-paste programming" anti-pattern.

Too often, programmers copy and paste code and then forget to make all the necessary changes in the pasted code. — Jim Suiter

After falling victim to this myself, I wanted to make it easier for everyone. Here is my alternative version:

// Filename: router.js
define([
 'jquery',
 'underscore',
 'backbone',
 'vm'
], function ($, _, Backbone, Vm) {
 var AppRouter = Backbone.Router.extend({
 initialize: function(options) {
 this.appView = options.appView;
 },
 
 register: function (route, name, path) {
 var self = this;
 
 this.route(route, name, function () {
 var args = arguments;
 
 require([path], function (module) {
 var options = null;
 var parameters = route.match(/[:\*]\w+/g);
 
 // Map the route parameters to options for the View.
 if (parameters) {
 options = {};
 _.each(parameters, function(name, index) {
 options[name.substring(1)] = args[index];
 });
 }
 
 var page = Vm.create(self.appView, name, module, options);
 page.render();
 });
 });
 }
 });
 
 var initialize = function(options){
 var router = new AppRouter(options);
 
 // Default route goes first
 router.register('*actions', 'DashboardPage', 'views/dashboard/page');
 router.register('optimize', 'OptimizePage', 'views/optimize/page');
 router.register('modules', 'ModulesPage', 'views/modules/page');
 router.register('backbone', 'BackbonePage', 'views/backbone/page');
 router.register('backbone/:section', 'BackbonePage', 'views/backbone/page');
 router.register('manager', 'ManagerPage', 'views/manager/page');
 
 Backbone.history.start();
 };
 
 return {
 initialize: initialize
 };
});

This interpretation of the AppRouter is more complicated, but developers don't have to modify it in order to add routes. Instead, they add a new call to "register" which takes three parameters, the route (URL patterm), the name, and the module path. The other nice feature is that the route parameters are automatically passed to the Backbone View as key-value pairs. I believe, if the boilerplate had the latest version of Backbone, then the 'backbone' and 'backbone/:section' routes could be consolidated into a single route, 'backbone(/:section)', which uses parentheses to denote 'section' as an optional parameter.

Using this routing approach was crucial for our project. The application has several routes and will continue to scale as more screens are ported from the legacy system. I hope you also find that this router streamlines your code and improves maintainability.