backend
Node Engines: Helping Developers Everywhere Avoid Phantom Bugs
Specific Node.js engines should be required for every package.json.
The classic opening line: “It works on my machine.”
Let me set the stage for you: you’re writing code, minding your own business, knocking out a shiny new feature for your web application when suddenly a red-light starts flashing (or the #support Slack channel lights up, either one) and it’s all hands on deck: there’s an issue in production. 🚨
As the development team drops everything to figure out what’s going wrong and attempts to pinpoint and recreate the issue, after twenty minutes of pointless debugging, one bold soul shrugs and says, "Works on my machine." 😒
This statement helps exactly no one, but accurately sums up the issues I’m about to discuss:
Something as simple as an incompatible Node version in production can break your application, and be nearly impossible for developers to locally figure out that’s the problem. Locking down your Node engine can help you quickly debug and avoid this.
You never give a second thought as to which version of Node or NPM your machine is using locally versus the version of Node.js your production environment is using… until it matters.
Real World Examples of Why This Matters
I speak from personal experience when I say, an AngularJS 1.5 application my team supports ONLY works in Node version 9, however, when we deployed to our production cloud environment, the Node buildpack that deployed with it, pulled in v6. Version 6?!? Who even develops with a Node version that old any more??
Regardless, our deployment failed because Node 6 couldn’t download the node_modules
dependencies correctly, the application failed to start up, and it took several hours of pouring over code, build logs, and environment variables to figure out what the actual problem was. That deployment became known as "Dark Thursday" within my dev team. To this day, if you ask someone who was there about that night, they’ll give you a haunted look in return. 😱
Similarly, our React application needs Node version 10 or higher, to take advantage of all the ES6 and beyond node dependencies that it has. It needs to be at least v10 or above, no ifs, ands, or buts.
So now you can see why not knowing what version of Node your cloud buildpack will default to is bad. It means you don’t know if your node modules will successfully be downloaded or fail. We can all agree that’s no way to develop.
But how do we ensure that we get the buildpack we need? How do we guarantee when we’re deploying from our local development environment to our cloud production environment that the Node.js runtime is the same? That it can support the Node dependencies our project needs to run?
It’s actually easier than you might think: strictly defining the Node engines for a project. Read on to learn how.
How do we prevent the wrong version of Node taking down our application?
Easy: engines
Engines? What does that even mean?
What are Node engines?
Node engines are a little discussed (but in my opinion pretty critical) configuration that can be specified in your package.json
file that tells anyone (or any machine) running the JavaScript application which version of Node is required for the code to work.
npm’s own docs say when defining engines:
“You can specify the version of node that your stuff works on” — npm
Pretty self-explanatory.
What this means in practice is: if engines
is included, when a JavaScript build deploys (depending on how the engine field is specified) it will look for a version of Node at or above the version configured in the package.json
, then download it and install all the node_modules
dependencies using that engine.
If an engine is not specified, the project is at the mercy of the buildpack gods, and will assume any old version of Node will do and download the dependencies with some random version of Node and npm. Which is how we end up with fire drills like the scenario described above.
In fact, you can actually specify the version of Node.js, the version of npm and the version of Yarn a project uses in its engines
config, which is pretty sweet.
Just to be crystal clear: the engines
field will be verified when you install the package, not when you run the application.
Now that you’re better acquainted with Node engines, why they’re useful and how they work, let’s go about setting up a project to prevent an easily avoidable deployment failure.
Step 1. Figure out your local development runtime
The first step is to figure out which version of Node you’re with developing locally. Simple enough.
Just open up a terminal window and type:
node -v
Then, you should see the version of Node you’re working with printed out to the terminal. See the screenshot below.
If you ever need to switch Node versions for local development, I’d highly recommend NVM (Node Version Manager), which I also wrote a blog post about here.
But that’s a tangent. All you need for now, is to know what Node version you’re currently developing in. Let’s move on to Step 2.
Step 2: Specify your Node engine in the package.json
Now that you know the version of Node, you can specify it in your package.json
file in your project. Below are a couple of examples what the engines config looks like.
Node and npm version example
This example includes both a Node version that’s greater than or equal to v11.10.2 and less than v12.0.0, and an npm version of exactly 5.8.0.
... // code above here like dependencies and other specs
"engines": {
"node": ">=11.10.2 <12.0.0",
"npm": "5.8.0"
},
... // more code below here
Node version example
Or this example, which includes a Node version around 10.15.0.
... // more code
"engines": {
"node": "~10.15.0"
},
... // more code
Specifying Node, npm and Yarn versions is similar to specifying them for dependencies in your package.json
, you can be as specific as you want down to the exact version, provide a lower limit, provide a range or an approximate. It’s up to you.
After you’ve set the Node engine in the package.json
, it’s time to deploy the application and verify the correct engine is being downloaded and used.
Step 3: Run your deployment build scripts & verify the buildpack matches
Once your build pipeline is deploying the JavaScript application, you should be able to verify in the logs that the specified Node version is being downloaded and the correct node module dependencies along with it.
Below are some logs from our Jenkins server that runs all of our builds for our AngularJS application, which requires a version of Node 9. As you can see in the first screenshot, the build is checking for a specific Node version on the server and finds 9.11.2 specified in the package.json
, so it installs that version to the build server if it’s not already available.
And here’s another screenshot example, this time of our React application, which needs a Node version of 10 or above, downloading Node v10.15.0, also specified in the React’s package.json
"engines"
field.
After this happens, the specified Node dependencies can be successfully downloaded and our applications should be able to start up without a problem.
Step 4: Enjoy your less stressful production deployment
Feel the relief wash over you as you suddenly have one less variable to worry about when you deploy to production. 😄
Conclusion
Node engines won’t solve all your problems, but they at least should take some of the guesswork (and unintended bugs) out of your production deployments. For the small amount of work it takes to specify this in your package.json
configuration, the investment now can save you hours down the line. Trust me, it’s worth it.
Check back in a few weeks, I’ll be writing about JavaScript, React or something else related to web development.
Thanks for reading, I hope this convinces you to specify your Node versions and npm / Yarn engines in your package.json
configuration.
References & Further Resources
Want to be notified first when I publish new content? Subscribe to my newsletter.