Skip to content

Instantly share code, notes, and snippets.

@caridy
Forked from ericf/gist:6133744
Created April 11, 2014 20:43
Show Gist options
  • Save caridy/10500131 to your computer and use it in GitHub Desktop.
Save caridy/10500131 to your computer and use it in GitHub Desktop.

Proposal for Extending Express Apps

Creating npm packages which extend the functionality of Express apps has become a major thing we've been doing. There are several approaches we can take, from messing with the Express object prototypes, to creating a function in which an express app is passed in. After trying the former, I'm now a fan of the latter.

The Issues with Extending Express

Extending the Express object prototypes has issues. The running Node.js process may have multiple versions of express, and in order to extend the prototypes you need to require('express'). This means that you might get a different express module instance than the one the main app is created from.

Both approaches suffer from extending something more than once. Similar to how there may be multiple version of express in the running Node.js process, there can also be multiple copies of the extension modules. If the app developer needs to rely on a different version of an Express extension than another component in the system, there's a potential for either the Express object prototypes or an app instance to get extended twice, with the wrong version of the extension.

Enter Branding

The solution I am proposing is to extend Express app instances by "branding" them. This concept is inspired by ES6 symbols, but can work today using a similar notation and behave in a similar way. To protect against extending the same app instance more than once, the extension can check for the existance of its "brand" on the app, if it's already there, the extension can return early. This is better described through an example:

express-state.js:

'use strict';

exports.extend = extendApp;

function extendApp(app) {
    // Early return because the app has already been extended.
    if (app['@state']) { return app; }
    
    // Brand it with the function that extended the app.
    Object.defineProperty(app, '@state', {value: exports});
    
    // Extend the app by adding this extension's methods.
    app.expose = app.response.expose = expose;
    // ...
    
    return app;
}

function expose() { ... }

app.js:

var express  = require('express'),
    expstate = require('express-state'),
    
    app = express();
    
// Extend the app with express-state.
expstate.extend(app);

console.log(typeof app.expose); // => "function"
console.log(app['@state']);     // => { local: 'state',
                                // =>   namespace: null,
                                // =>   extend: [Function: extendApp] }

Why Object.defineProperty()?

Object.defineProperty() is an ES5 feature which allows you to define and initialize properties on an object. By default properties have the following configuration:

  • non-writable
  • non-configurable
  • non-enumerable

The attirbutes make it perfect for branding! The brand will not show up in Object.keys() (because they are non-enumerable), but when you inspect branded objects in a tool like node-inspector they will show up, handy for debugging!

Why @-names?

Using @-names aligns with ES6 symbol notation which also start with @. They also show up at the top of the properties list in the inspector. And they are not likely to collide with other properties on the object.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment