Best Practices for Node.js Development
Last updated May 15, 2024
Table of Contents
This material is a curated and maintained version of a blog post.
Start Every New Project with NPM’s Init
npm’s init
command scaffolds out a valid package.json for your project, inferring common properties from the working directory.
$ mkdir example-app
$ cd example-app
$ npm init --yes
Run it with the --yes
flag, and then open package.json
to make changes. First, specify an engines
key with your version of node (node -v
).
"engines": {
"node": "20.x"
}
Stick with Lowercase
Some languages encourage filenames that match class names, like MyClass
and ‘MyClass.js’. Node.js uses lowercase files.
let MyClass = require('my-class');
Node.js is the rare example of a Linux-centric tool with great cross-platform support. While OSX and Windows treat myclass.js
and MyClass.js
equivalently, Linux doesn’t. To write code that’s portable between platforms, match require
statements exactly, including capitalization.
The easy way is to stick with lowercase filenames for everything. For example, my-class.js
.
Cluster Your App
Because the Node runtime is limited to a single CPU core and ~1.5 GB of memory, deploying a non-clustered node app on a large server is a waste of resources.
To take advantage of multiple cores and memory beyond 1.5 GB, add Cluster support into your app. Even if you’re only running a single process on small hardware today, clustering gives you easy flexibility for the future.
Testing is the best way to determine the ideal number of clustered processes for your app, but it’s good to start with the reasonable defaults offered by your platform, with a simple fallback. For example:
const CONCURRENCY = process.env.WEB_CONCURRENCY || 1;
Choose a Cluster abstraction to avoid reinventing process management. If you want to separate your main and worker files, try Forky. If you prefer a single entrypoint file and function, look at Throng. You can read more about Throng in Optimizing Node.js Concurrency.
Be Environmentally Aware
Don’t litter your project with environment-specific config files. Instead, take advantage of environment variables.
To provide a local development environment, create a .gitignore’d .env
file, which heroku local
loads.
DATABASE_URL='postgres://localhost/foobar'
HTTP_TIMEOUT=10000
Start your app with heroku local
. It automatically pulls in these environment variables into your app under process.env.DATABASE_URL
and process.env.HTTP_TIMEOUT
. When you deploy your project, it automatically adapts to the variables on its new host.
This way is simpler and more flexible than config/abby-dev.js
, config/brian-dev.js
, config/qa1.js
, config/qa2.js
, and config/prod.js
, etc.
Avoid Garbage
Node (V8) uses a lazy and greedy garbage collector. With its default limit of about 1.5 GB, it sometimes waits until it absolutely must reclaim unused memory. If your memory usage is increasing, it’s possible that it’s not a leak, but rather Node’s usual lazy behavior.
To gain more control over your app’s garbage collector, you can provide flags to V8 in your Procfile
.
web: node --optimize_for_size --max_old_space_size=920 --gc_interval=100 server.js
This control is especially important if your app runs in an environment with less than 1.5 GB of available memory. For example, if you want to tailor Node to a 512 MB container, try:
web: node --optimize_for_size --max_old_space_size=460 --gc_interval=100 server.js
Hook Things Up
Lifecycle scripts make great hooks for automation. Heroku provides custom hooks so that you can run custom commands before or after we install your dependencies. To run something before building your app, you can use the heroku-prebuild
script. To build assets with grunt
, gulp
, browserify
, or webpack
, do it in a build
script.
In package.json
:
"scripts": {
"build": "bower install && grunt build",
"start": "nf start"
}
You can also use environment variables to control these scripts.
"build": "if [ $BUILD_ASSETS ]; then npm run build-assets; fi",
"build-assets": "bower install && grunt build"
If your scripts start getting out of control, move them to files.
"build": "scripts/build.sh"
Scripts in package.json
automatically have ./node_modules/.bin
added to their PATH
so that you can execute binaries like bower
or webpack
directly.
Only Git the Important Bits
Most apps consist of necessary files and generated files. When using a source control system like Git, avoid tracking anything generated.
For example, your node app probably has a node_modules
directory for dependencies, which you want to keep out of Git. Add these files and directories to .gitignore
.
As long as each dependency is listed in package.json
, anyone can create a working local copy of your app, including node_modules
, by running npm install
.
Tracking generated files leads to unnecessary noise and bloat in your Git history. Worse, because some dependencies are native and must be compiled. Checking them in makes your app less portable because you provide builds from just a single, and possibly incorrect, architecture.
For the same reason, don’t check in bower_components
or the compiled assets from grunt
builds.
If you accidentally checked in node_modules
before, that’s okay. You can remove it.
$ echo 'node_modules' >> .gitignore
$ git rm -r --cached node_modules
$ git commit -am 'ignore node_modules'
Also, ignore npm
’s logs so that they don’t clutter the code.
$ echo 'npm-debug.log' >> .gitignore
$ git commit -am 'ignore npm-debug'
By ignoring these unnecessary files, your repositories are smaller, your commits are simpler, and you avoid merge conflicts in the generated directories.