Layers - Layered Architecture for node.js made easy

By Dave Elkan

EDIT - This guide is kinda out of date and probably not that applicable anymore. Try using a supported library like Flatiron or just plain old Express but by no means let me stop you if you want to take Layers and run with it.

In my previous post A Layered Node.js Architecture using Express I wrote on the benefits of using a layered architecture and how to implement one in a node.js web app. This post goes one step further by introducing Layers, a module which will help automatically load and neatly setup the layers (and routes) of your web app.

Layers currently only supports Express, but adding support for other frameworks is simple. Feel free to submit a pull request!

The following is a description of how Layers works. For the impatient or code-hungry there is the working Layered Express example from which the following snippets are taken.

Layers performs two basic operations: - Loads all layer javascript files. - Loosely couples the layers and adds routes.

Installing

$ npm install layers

Example app

Simply require it and instantiate with your app, the path to your layers, a function which returns your wiring (more on this in the next section) and, optionally, an options object. i.e.

var Layers = require('layers').Express,
    wiring = require('./layers/wiring');
new Layers(app, __dirname + '/layers', wiring);

File Layout

In this example the layout of the directories is:

__dirname
   - layers
       - controllers
       - services
       - views

Loading Layers

Each directory nested immediately within the layers directory defines a layer.

Each of these layer directories are recursively scanned for javascript files which are imported using require. The result of the require is inspected for two possibilities:

  • An object
  • A function which returns an object.

The object loaded or returned from the function is appended to the app object under the layer’s namespace.

e.g. /path/to/app/layers/controllers/BookController.js becomes app.controllers.bookController /path/to/app/layers/services/AuthorService.js becomes app.services.authorService

One exception is made when loading the layer files: Any file whose name begins with “Base” is ignored. This name can be overridden by specifying the excludePrefix option.

Wiring

To wire the layers together you need to create a ‘wiring’ function. This function takes the layer populated app object as it’s only parameter and returns a hash of arrays indexed by the route they service. i.e.

module.exports = function(app) {
    var controllers = app.controllers,
        views = app.views;

    return {

        "/": [{
                action: controllers.homeController.getAuthorAndBookList,
                views: {
                    html: views.homeView
                }
            }
        ],

        "/books": [{
                action: controllers.bookController.getBookIndex,
                views: {
                    html: views.bookIndexView,
                    json: views.jsonView
                }
            }
        ],

        "/book/:key": [{
                action: controllers.bookController.getBookByKey,
                views: {
                    html: views.bookView,
                    json: views.jsonView
                }
            }
        ],

        "/authors": [{
                action: controllers.authorController.displayAuthorList,
                views: {
                    html: views.authorIndexView,
                    json: views.jsonView
                }
            }
        ],

        "/authors/:key": [{
                action: controllers.authorController.displayAuthorByKey,
                views: {
                    html: views.authorView
                }
            }
        ]
    };
};

The format of the wiring object is important. Each route supports many handlers which all have to define action and view properties.

Actions

A handler’s Action is simply a reference to a function which accepts the request and response objects as well as the callback and next functions. They are called in the context of the app object which allows for easy access to all of the other layers.

Action functions generally belong to the Controller layer. They are responsible for pulling variables from the request, their validation and sending them to the Service layer. This contrived example does not allow any '1s’ in the Author’s key which is silly, but stop and imagine authentication at this point.

You will also notice that this controller simply delegates to the service layer, but at the same time it shields it from the request and response objects.

The callback variable which is owned and created by Layers is passed straight through to the service layer.

module.exports = {

    displayAuthorList: function(req, res, callback) {
        this.services.authorService.getList(callback);
    },

    displayAuthorByKey: function(req, res, callback, next) {
        var authorKey = req.params.key;
        if (authorKey.indexOf("1") > -1) {
            callback(new Error("No ones allowed!"));
        } else {
            this.services.authorService.getAuthorAndTheirBooks(authorKey, callback);
        }
    }
};

Views

Views are wrappers around your favourite templating system. They also accept the request and response objects but also a third object which is the result of the action. This object is the payload sent to the template.

BaseExpressView can be used as a basis for your own Express (jade) Views. You only need to specify the “getTemplate” function.

Conclusion

Layers is a simple way to get your app up and running quickly in something which resembles a scalable way. It allows you to specify all of your routes in one place and to specify an ordered list of actions for each of them. It will load and run your layer code and namespace it on your app object for easy access.

Love to hear some feedback if you have any.

comments powered by Disqus