Intro

A while ago, I created a tiny Github repository for the sole purpose of helping people introduce a means of separating concerns regarding routes in Express.js. The project introduced controllers, and a routes.js file.

The syntax for it would look a little something like the following:

controllers/something.js


exports.index = function(req, res) {
  res.send('Default route');
};

exports.new = function(req, res) {
  res.send('A form to create a new thing');
};

exports.create = function(req, res) {
  res.send('Route for creating a new thing');
};

exports.show = function(req, res) {
  res.send('Route for showing a single thing');
};

exports.edit = function(req, res) {
  res.send('Form for editing an existing thing');
};

exports.update = function(req, res) {
  res.send('Route to update an edited existing thing');
};

exports.destroy = function(req, res) {
  res.send('Route to delete an existing thing');
};

This dynamically generates the routes:


app.get('/something/', function(req, res) {
  res.send('Default route');
});

app.get('/something/new', function(req, res) {
  res.send('A form to create a new thing');
});

app.post('/something/', function(req, res) {
  res.send('Route for creating a new thing');
});

app.get('/something/:id', function(req, res) {
  res.send('Route for showing a single thing');
});

app.get('/something/:id/edit', function(req, res) {
  res.send('Form for editing an existing thing');
});

app.put('/something/:id', function(req, res) {
  res.send('Route to update an edited existing thing');
});

app.delete('/something/:id', function(req, res) {
  res.send('Route to delete an existing thing');
});

The Problem

The problem with this syntax is that it offers little room for configuration. The examples provided above apply only to default CRUD actions. If you wanted to handle a route that had nothing to do with CRUD operations, you would have to create a controller and action and manually point to it in a root-level routes.js file:


var routes = {
  'get /about': {
    controller: 'Site',
    action: 'about'
  }
};

module.exports = routes;

Not only that, but the syntax for adding middleware to an action is a little messy:


exports.actionWithMiddleware = [
  firstMiddleWare,
  secondMiddleware,
  function (req, res) {
    res.send('Route with middleware');
  }
];

Furthermore, there is no way of specifying dynamic route parameters other than the default :id for any CRUD operations.

This project that tried to solve a problem did only one thing, and that was introduce leaky abstractions into what would otherwise be a solid code base.

Failed Solutions

As I worked with this paradigm, I started to realize the many pitfalls it introduced, so I set out to brainstorm how to improve it. I threw together this brainstorm in CoffeeScript:


routes =

# points to controller#action
# overwrites original URL by default
'/banana': 'fruit#banana'

# use alias: true to disable overwriting of URL
'/greeting':
  action: 'site#greeting'
  alias: true

module.exports = routes



SiteController =

  # makes path '/posts/site/greeting' instead of '/site/greeting'
  # but actually references a separate controller if it exists
  # so it can check that for nesting too and include it in the route
  # else it just uses the word
  nestUnder: 'posts'

  greeting: (req, res) -> res.send 'Hello World!'

  # change the query parameter
  show:
    param: ':user'
    action: (req, res) ->
      User.findOne user: req.params.user, (error, user) ->
        res.send unless error then user else error

  # use a method other than GET
  logout:
    method: 'post'
    action: (req, res) -> do req.logout

  # a policy points to this action and ensures
  # that the user is an admin before it proceeds
  admin:
    policies: 'isLoggedIn isAdmin'
    action: (req, res) -> res.render 'admin'

module.exports = SiteController

You can see that this syntax is somewhat inspired by Sails.js. The problem with this was that if it were written in vanilla JavaScript, the resulting code would be a mess of nested objects, privy to Callback Hell.

So, I brainstormed even further and started messing around with Angular.js-style dependency injection for the sole purpose of simplifying the syntax.

Theoretically, the code for a single controller would look like this:


exports.index = function(req, res, $scope) {
  $scope.path = '/:user/profile';
  $scope.middleware = ['isLoggedIn'];
  $scope.method = 'get';
  res.send('A user profile page');
}

The problem with this syntax, though, is that the code would need to be inspected outside of the scope of the action, and the only way to do that would be to use eval() (which is a very bad practice). I set out to StackOverflow to get some help on the issue, and found very little assistance.

Not to mention the performance issues involved in a dynamic setup like that.

So, I decided to give up on bending computers to my will, and focused on

The Real, Bare-Bones Working Solution

And it's actually rather simple and robust. The first step is to create a controllers/ directory in your app root (where your server.js is located).

In that directory, create an index.js file:


module.exports = function(app) {

  'use strict';

  var fs = require('fs');

  fs.readdir('./controllers', loadControllers);

  function loadControllers(error, files) {
    if (error) {
      throw error;
    } else {
      files.forEach(requireController);
    }
  }

  function requireController(file) {
    // remove the file extension
    var controller = file.substr(0, file.lastIndexOf('.'));
    // do not require index.js (this file)
    if (controller !== 'index') {
      // require the controller
      require('./' + controller)(app);
    }
  }

};

What's happening here is that the index.js file exports itself as a module that expects to recieve the app object from Express.

It then requires the file-system API, and reads the directory that it is in for any controllers (making sure to exclude itself from the list of files to prevent any circular dependency issues) and requires them, passing them the app object.

Very simple.

From there, creating a controller is as simple as this:

Create a site.js file in the controllers/ directory. This file will hold all routes that relate to root-level pages such as the home page, the about page and so on. We're just going to focus on the home page to keep things simple.


module.exports = function(app) {

  app.get('/', function(req, res) {
    res.render('home'); // render home page view
  });

};

The final step to getting this to work is to simply require the controller index.js module within the file that creates your server, whether it be named app.js or server.js. Be sure to include this after your app.configure calls and before your app.listen or server.listen calls:


// load routes
require('./controllers')(app);

Everything should work as expected, now. This setup benefits from no leaky abstractions and you have full controll over configuring your routes, while at the same time maintaining a rock-solid separation of concerns.

I hope you enjoy. Thank you for reading. :)

EDIT: This concept has now found it's way into a module, created by Matt Farmer and the module can be found on this Github repository.

comments powered by Disqus