Like any kind of apps, JavaScript apps also have to be written well.
Otherwise, we run into all kinds of issues later on.
In this article, we’ll look at some best practices we should follow when writing Node apps.
DevOps Tools
DevOps tools will make configuring our environment and server setup easier.
Also, we need a process manager to restart our Express app automatically in case it crashes.
And we need a reverse proxy and load balancer to expose our app to the Internet, cache requests, and balance the load across multiple worker processes.
This lets us maintain high performance in our app.
Managing Environment Variables in Node.js with dotenv
The dotenv library is very useful for letting us read variables from an .env
file
For example, we can write:
NODE_ENV=production
DEBUG=false
in our .env
file.
And then we can read it with:
require('dotenv').config()
const express = require('express')
const app = express()
It’ll read the environment variables from a file named .env
by default.
However, it can also read from multiple environment variable files.
Make Sure the Application Restarts Automatically with a Process Manager
If we have an Express app, it’ll crash when an unhandled error is encountered.
Therefore, it’s important that we restart our app automatically so that it won’t be down.
We can do that with Systemd by creating a new file in /lib/systemd/system
called app.service
:
[Unit]
Description=Node.js as a system service.
Documentation=https://example.com
After=network.target
[Service]
Type=simple
User=ubuntu
ExecStart=/usr/bin/node /my-app/server.js
Restart=on-failure
[Install]
WantedBy=multi-user.target
The Service
section has the ExecStart
line which runs our app on startup.
It also has the Restart
line to restart on failure.
We can then reload the daemon start the script with:
systemctl daemon-reload
systemctl start fooapp
systemctl enable fooapp
systemctl status fooapp
PM2
PM2 is a process manager that lets us manage the Express app process.
We can install it by running:
npm i -g pm2
Then we can run our app by running:
pm2 start server.js -i max
-i max
is the max number of threads to run our app with.
This way, it’ll spawn enough workers to use all CPU cores.
Load Balancing and Reverse Proxies
The Node cluster module lets us spawn worker processes that serve our app.
We can create a cluster by writing:
const cluster = require('cluster')
const numCPUs = require('os').cpus().length
const app = require('./src/app')
const port = process.env.PORT || 8888
const masterProcess = () => Array.from(Array(numCPUs)).map(cluster.fork)
const childProcess = () => app.listen(port)
if (cluster.isMaster) {
masterProcess()
} else {
childProcess()
}
cluster.on('exit', () => cluster.fork())
The master process creates the cluster and the child presses listen for requests at the given port.
The masterProcess
counts how many CPU cores there are and calls cluser.fork
to create child processes equal to the number of cores available.
The exit
event listener will restart the process if it fails with cluster.fork
.
Then in our Systemd file, we replace:
ExecStart=/usr/bin/node /my-app/server.js
with:
ExecStart=/usr/bin/node /my-app/cluster.js
And then we can restart Systemd with:
systemctl daemon-reload
systemctl restart fooapp
Conclusion
We can read environment variables with dotenv and add clusters with the cluster
module.