public
Last active

  • Download Gist
gistfile1.md
Markdown

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.

this is definitely simpler than our previous approach with augment() and express().

about extend(), we either return a reference to the extended app, or we return nothing, but the current behavior will be confusing for users who just want to guarantee that the app instance is correctly extended, they don't care if it was extended already or not, in which case the result should be the same.

@caridy I think we should have the extend methods return the app instance. For the other part—was this thing extended—do you want to offer a function which checks, or host the "brand" so people can check themselves?

No, I don't think we need to provide an API to detect whether or not it was extended already. If you need the app to be extended with a plugin, you just call for extend. If, for whatever reason, they want to check it, they can do it themselves.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.