Organizing Express Routes

Express, the Sinatra of Node.js web frameworks, is a piece of software I’m very fond of for its lack of enforced structure. You can structure your Express.js application any way you like. But like the dude said to Spider Man,

“With great power comes great responsibility”

I’ve worked through quite a few iterations of different Express apps from real tiny ones to very large scale ones and I thought that maybe it would help someone one day if I shared how I organize my Express routes from the file structure to the actual code.

Project structure

Let’s start off with my project structure. You’ll likely need to configure your development environment and you should also have a build tool set up to help with repetitive development tasks and build/deploy tasks. With that in mind, this is how I structure these applications:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ProjectRoot
  |- migrations/
  |- server/
  | |- config/
  | |- lib/
  | |- middleware/
  | |- models/
  | |- public/
  | |- routes/
  | |  |- index.js
  | |  |- (all other route files here)
  | |- views/
  |- test/
  |- .gitignore
  |- README.md
  |- gulpfile.js

That’s not everything of course but the important thing to look at is the server/routes folder. In it we have a file called index.js. This file is responsible for require()ing all of our other route files. It allows us to break up routes into separate modules. Before I talk about how to organize routes properly I’ll show you some ways which don’t work.

How not to write routes

Method 1: Everything on the app object

In simple Express tutorials you’ll see an entire Express application set up like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var express = require('express'),
    app     = express();

// Now define routes
app.get('/', function(req, res, next) {
  // do something
});

// Or, some people think this is more effiecient, which it is
// but still not good enough:
app.route('/')
  .get(function(req, res, next) {
    // do stuff
  })
  .post(function(req, res, next) {
    // do other things
  });

var server = app.listen(3000, function() {
  console.log('App started');
});

This works great when you have just a couple of routes but it’ll fall apart and get messy fast. Using app.route() is an improvement but the problem is that you still have all of your routes defined in your main server startup script. You’ve got to break it out of there.

Method 2: The easy module way

The next thing you could try is by making all of your routes modules and break them into separate files. This actually works really well but you’ll soon notice that there’s something off about this. It just doesn’t feel right and when something doesn’t feel right in your code there’s definitely a better way to do this. I structured a very large scale Node application in this way and we did keep things organized but the routes themselves were full of messy code and it was confusing to understand how some objects made their way into the routes. Here’s what that looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// main server file
var express  = require('express'),
    app      = express(),
    config   = require('my-config-module'),
    database = require('some-database-module');

// let's suppose we already set up our middleware 
// and settings here

// load up our routes
require('./routes')(app, db, logger);

var server = app.listen(3000, function() {
  console.log('App started');
});

So before we go on, let’s go over what this is doing. You’ll see that when we load up our routes we’re using it as a function and passing it our app, database, and config objects. This is because routes/index.js will be exporting a function that takes these arguments and uses them to further require more files and set up our routes. Here’s what routes/index.js looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// routes/index.js
var fs          = require('fs'),
validFileTypes  = ['js'];

var requireFiles = function (directory, app, config, db) {
  fs.readdirSync(directory).forEach(function (fileName) {
    // Recurse if directory
    if(fs.lstatSync(directory + '/' + fileName).isDirectory()) {
      requireFiles(directory + '/' + fileName, app, config, db);
    } else {

      // Skip this file
      if(fileName === 'index.js' && directory === __dirname) return;

      // Skip unknown filetypes
      if(validFileTypes.indexOf(fileName.split('.').pop()) === -1) return;

      // Require the file.
      require(directory + '/' + fileName)(app, config, db);
    }
  });
};

module.exports = function (app, config, db) {
  requireFiles(__dirname, app, config, db);
};

There’s a lot of code there but it basically does this:

  1. Loop through all files in the routes folder
  2. If the files found are JS files and not named index.js then require them and run them as functions passing in the app, database, and config objects for the routes to use
  3. If what’s found is a folder then recurse into it and run steps 1 and 2

There’s a third part to this, the actual routes themselves. Above we’re seeing a utility that’s requiring routes but here’s what the actual routes themselves look like:

1
2
3
4
5
6
7
8
// routes/users.js (a users controller)
module.exports = function(app, config, db) {
  app.route('/users/?')
    .get(/* route code here */)
    .post(/* route code here */)
    .patch(/* route code here */)
    .delete(/* route code here */);
};

The route is pretty simple but it requires you to pass around these sometimes heavy objects from the server start script all the way to the route itself. This works but there’s a better way. Instead why don’t we use Express’ Router() object and set up our routes as a series of middleware.

Method 3: The best way I know of

Rather than putting all of our routing logic directly on the app object and passing around all sorts of heavy objects on each request, we can instead set up our routes as a series of middleware that rely on Express’ Router() functionality.

1
2
3
4
5
6
7
8
9
10
11
12
13
// main server file
var express  = require('express'),
    app      = express();

// let's suppose we already set up our middleware 
// and settings here

// load up our routes
require('./routes')(app);

var server = app.listen(3000, function() {
  console.log('App started');
});

So far it looks much like what we had before and yes, we are passing the app object to the route index file. But that’s the extent of passing around heavy objects. It doesn’t go any further than that. Here’s the route index file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// routes/index.js
var _         = require('lodash'),
    fs        = require('fs'),
    excluded  = ['index'];

module.exports = function(app) {
  fs.readdirSync(__dirname).forEach(function(file) {
    // Remove extension from file name
    var basename = file.split('.')[0];

    // Only load files that aren't directories and aren't blacklisted
    if (!fs.lstatSync(__dirname + '/' + file).isDirectory() && !_.includes(excluded, file)) {
      app.use('/' + basename, require('./' + file));
    }
  });
};

Here we have a list of blacklisted file names. The code here runs through all of the files in the routes/ directory and then mounts each one as an Express middleware at the URL prefix that matches the file’s base name. So for example, if a route file is named users.js then we’ll mount the file’s routes at /users. It ends up being translated as app.use('/users', requiredFile).

Now we can start treating our routes as true controllers and have separate middleware for them if we need it as well. Here’s what the actual route file looks like:

1
2
3
4
5
6
7
8
9
10
// routes/users.js
var express           = require('express'),
    UsersController   = express.Router();

UsersController.route('/?')
  .get(/*...*/)
  .post(/*...*/)
  .delete(/*...*/);

module.exports = UsersController;

This route would be mounted at /users and then you can work with each route/controller as if it were its own application (but remember that global middleware will still be applied to it).

And there it is. The best way to structure Express routes. Another bonus to this is that it makes it trivially easy to mount separate versioned APIs in the future if you wanted. I may write more about doing this in the future. Hopefully that all made sense. Enjoy your more organized routes.

Web development

« How to structure Bookshelf.js models How to learn to code »

Comments