bentomas.com

Bomber – node.js web framework — URL Routing

This weekend I finally got a chance to play around a little bit with some of the ideas I talked about in my last post on writing a new web framework with node.js.

Introducing Bomber

I have decided to call the framework “Bomber”, which is a variant spelling of “Bomer”, which is a combination of “Ben” and “Omer”, which is because my friend Omer has been volunteering API/design thoughts and advice.

You can checkout the source code for Bomber from GitHub if you want to see how it works or try it out. You must have node.js installed1 first.

Bomber is centered around the idea of “apps” (much like Django is), where an app is just a way to group code together in a folder so that it is more portable/reusable. Unlike with Django, Bomber expects certain code to live in certain places. For example, the URL routing has to be in a file called routes.js. I have written up a description of what files an app can have in the docs folder of the source.

To actually run the Bomber server, cd into the source code directory and run the command ./bomber.js ./exampleProject. This will start the server, using the ‘exampleProject’ app.

Right now Bomber doesn’t do much. If you are looking for something more stable may I suggest one of the more established projects?

Bomber Routing

I’ll show you how it works first, and then after, I’ll talk about why I decided to have it work the way it does.

The goal of the routing in Bomber is to determine which function should be run given a URL.

Here is an example of what a routes.js file could look like:

var Router = require('bomber/lib/router').Router;

var r = new Router();

r.add('/', { view: 'simple', action: 'index' });
r.add('/section', { view: 'simple', action: 'section' });
r.add('/section/:id', { view: 'simple', action: 'show' });
r.add('/:view/:action');
r.add('/other_app/:action', { view: 'view_name' });

exports.router = r;

All this file needs to do, is create a Router object, add routes to it, and then finally export that router object.

If you have used Rails this should look very familiar.

How adding the routes works is you give the router the URL to match, and then an object with the set of parameters that should be used to decide which function to run.

The app parameter tell Bomber which app to use, view tells it what view file to look in, and action tells it the name of the function to run. Any other parameters (or those declared in the URL, like :id) will find their way to the function (I’m not sure how yet, wait for another post!). If you don’t specify app, Bomber will use the current app.

Additionally, if you want complete control over the process, you can give it a regular expression instead of a string for the URL to match. Behind the scenes all routes are converted to regular expressions, anyway.

The Thought Process

There were 3 ways I considered using when designing this: the Sinatra way, the Django way, and the Rails Way.

The Sinatra way is very popular with node.js frameworks (I have yet to find one that doesn’t do it this way). The Sinatra way is to have the routing in the same file and even inline with the code that is to be run. It looks something like this (to take an example from Express):

get('user/:name/:operation', function(){
  param('operation') + 'ing ' + param('name')
})

Or like this (to take an example from Djangode):

dj.makeApp([
  ['^/$', function(req, res) {
    dj.respond(res, '<h1>Homepage</h1>');
  }],
  ['^/other$', function(req, res) {
    dj.respond(res, '<h1>Other page</h1>');
  }],
  ['^/page/(\\d+)$', function(req, res, page) {
    dj.respond(res, '<h1>Page ' + page + '</h1>');
  }]
]);

This is a great system, as long as a) you don’t have too many routes and b) your functions aren’t too large. Otherwise I feel like it gets a bit unwieldy trying to keep track of what happens where. I’m kind of envisioning Bomber being used for slightly larger projects. And in that case it is nice being able to see all your routes at one glance.

This means I’d like the routes to be in a separate file from the functions.

The Django way works like this. You have a file in which you declare your routes, and each route maps to a specific function else where in the app. Here’s an example from the docs:

urlpatterns = patterns('',
    (r'^articles/2003/$', 'news.views.special_case_2003'),
    (r'^articles/(\d{4})/$', 'news.views.year_archive'),
    (r'^articles/(\d{4})/(\d{2})/$', 'news.views.month_archive'),
    (r'^articles/(\d{4})/(\d{2})/(\d+)/$', 'news.views.article_detail'),
)

I have two complaints about the way Django does routing.

  1. I have to write out a route for every different function I want to use.
  2. Named groups aren’t very elegant (and this is Python’s fault not Django’s).

But there are two things I really like about the way Python does routing.

  1. It is great being able to just write a regular expression. I like that power.
  2. You can pass off the processing of a URL to other routing files. Here’s the example from the documentation:
urlpatterns = patterns('',
    (r'^weblog/',        include('django_website.apps.blog.urls.blog')),
    (r'^documentation/', include('django_website.apps.docs.urls.docs')),
    (r'^comments/',      include('django.contrib.comments.urls')),
)

This is really great. And is something I hope to add to the routing for Bomber soon.

The Rails way works very similar to how I ended up doing routing in Bomber.

It is also very similar to how Django does its routing. There is a subtle difference, however. With Rails, instead of having a route match to a function, a route just generates an object which has all the parameters needed to find the function. Then Rails uses this object to actually do that. The difference is that you can use parameters gathered from the URL to tell Rails which (to use Rails lingo) controller to use, or which action to run. This means you can set up defaults and not have to do anything else (if you want to). Even if you add controllers or actions later, your defaults still handle everything. Here’s an example from the Rails Guide on Routing:

map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'

This solves my first complain about Django.

I also like it much better how you don’t have to think about regular expressions if you don’t want to. “Give me everything between this slash and this one and call it X” seems so intuitive to me. This solves my second complaint about Django.

In the end I am hoping to have a system that takes the best parts from Django and Rails, and I think I am on my way.

(I can’t link directly to that section, scroll down). And there is a Mac OS X installer if that is easier for you. But the installer will soon become out of date. It will probably be easiest to stay current if you checkout and build from source. Node is changing quickly these days!

  1. Simon Willison has a great write up of how to get it installed