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.
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.
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] }
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!
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.
@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?