Recently I’ve noticed a lack of resources on advanced Node.js topics. There are plenty of guides and tutorials for getting started, but very little is written on maintainable design or scalable architecture. So I created The Node.js Handbook, a series meant to address this gap by sharing tribal knowledge and best practices. You can read more here.
Before jumping in, I want to explicitly point out that almost all of the concepts explained below should be avoided in production. These patterns have the potential to cause nightmares for you or your team down the road, with hidden bugs and unexpected side-effects. But they exist for a reason, and when used properly (read: very carefully) they can solve real problems that other, safer patterns can’t. But just… you know… with those terrible, dangerous side effects.
Modifying already-existing methods can be a little trickier. You can simply overwrite them, but if you want to leverage the original function you’ll need to save it first. Using a more practical example than the one above, you may want to attach data to every every template that gets rendered in an Express application:
1 2 3 4 5 6 7
This practice is called monkey patching, and it is generally considered to be a terrible idea. Monkey patches pollute your application’s shared environment. They can collide with other patches, and be impossible to debug even when working properly. The pattern is a powerful hack, but luckily its adoption and use is generally limited.
But desperate times can call for desperate measures, and sometimes a monkey patch is necessary. If the situation allows it, building your patch as a separate module will help keep the hack quarantined and decoupled from the rest of your application. Organizing your monkey patches in one place can also make it easier to find when/if debugging is needed.
The first thing you’ll want to do is make as many assertions about the environment as possible. Assert that the method you’re adding/modifying hasn’t been added/modified yet. Check that its version is correct. Check that everything exists exactly as you expect. Check all of this first, and throw an error if any of it doesn’t look right. While this might sound over-the-top now, it could save you days of debugging later if you fail to catch some subtle collision.
You should also consider exporting your monkey patch as a singleton, with a single
apply() method that executes the code. Applying the patch explicitly (instead of as a side effect of loading it) will make your module’s purpose clearer. It will also allow you to pass arguments to your monkey patch, which might be helpful or even necessary depending on your use case.
1 2 3 4 5 6 7 8 9
Polyfills are most commonly found on the client-side, where different browsers have different levels of feature support. Instead of forcing your application down to support the lowest-common denominator (looking at you, IE) you can use a polyfill to add new features to old browsers and standardize across platforms.
As a server-side developer, you might think that you’re safe from this problem. But with Node’s long v0.12 development cycle, even Node.js developers will find new features that aren’t fully available to them yet. For example, async-listeners were added in v0.11.9, but you’ll have to wait until v0.12.0 before you’ll see them in a stable build.
Or… you could consider using an async-listener polyfill.
The polyfill is still a monkey patch at heart, but it can be much safer to apply in practice. Instead of modifying anything and everything, polyfills are limited to implementing an already-defined feature. The presence of a spec makes polyfills easier to accept, but all the same warnings and guidelines for monkey patching still apply. Understand the code you’re adding, watch out for collisions (specs can always change), and make sure you assert as much as possible about your environment before applying the patch.
1 2 3 4 5 6 7 8 9
This feature can be powerful, but don’t refactor your code just yet. Modules are loaded synchronously, which means nothing else can run while the file is loaded and parsed. And once parsed, the result is saved and persisted in your module cache for the rest of your applications lifetime. Unless you intend to actually interact with the object as a module, stick to
JSON.parse(), and save yourself the performance hit and added complexity.
Node supports JSON right out of the box, but
require() will throw an error if you try loading anything else. However, if you roll up your sleeves and start poking around, you’ll find that Node can be made to support any number of file types, as long as you provide the parsers.
Here’s how it works: Node holds a collection of “file extensions” internally, which are responsible for loading, parsing, and exporting a valid representation of a given file. The native JSON extension, for example, reads the file via
fs.readFileSync(), parses the results via
JSON.parse(), and then attaches the final object to
module.exports. While these parsers are private to Node’s Module type, they are exposed to developers via the
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Note: This feature was deprecated once everyone realized that processing your code into JS and JSON before run-time is almost always the better way to go. Parsing directly during runtime can make bugs harder to find, since you can’t see the actual JS/JSON that gets generated.
It would be too easy to just load and return the file contents as an MP3 module, so lets go one step further. In addition to getting the MP3 file contents, the file extension should also generate song metadata (such as title and artist) via the audio-metadata module.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
Depending on the use case, this extension could be built to add even more functionality like streaming, playing, and otherwise interacting with the song, all automatically supported at load time.
This post isn’t meant to endorse or approve of any of the above patterns, but it isn’t a blanket denouncement either. What makes these modules dangerous is the same thing that makes them so powerful: they don’t follow the normal rules. Polyfills can update your feature set without actually updating the framework, while File Extensions change the idea of what a Node.js module can actually be. Understanding how any of this is possible will help you make smarter decisions when it comes to module design, and allow you to spot potential problems before they happen.
And one day, when you find yourself in a jam, one of these patterns might just help you out.