Categories
Node.js Best Practices

Node.js Best Practices — Errors, Code, and Resources

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.

Handle Errors and Exceptions Properly

We should handle errors and exceptions properly on our Express app.

In synchronous code or an async function, we can use try-catch:

async function foo() {
  try {
    const baz = await bar()
    return baz
  } catch (err) {
    console.error(err);
  }
}

If we want to catch errors in middleware, we can create our own middleware to catch errors.

For example, we can write:

function errorHandler(err, req, res, next) {
  console.error(err)
  res.status(err.status || 500).send(err.message)
}

router.use(errorHandler)

We have the errorHandler middleware to catch errors from other middleware that are added before it.

Then we can call router.use or app.use with the errorHandler to handle the error.

Watch Out For Memory Leaks

We should watch for memory leaks so that our app doesn’t run out of memory.

Increasing memory usage is bad if it happens continuously.

This means that the app keeps using memory.

There’re apps like Sematext Agent Express module which lets us watch the CPU and memory usage of our app.

With this package, we can integrate it with:

const { stMonitor, stLogger, stHttpLoggerMiddleware } =
require('sematext-agent-express')
stMonitor.start()

const express = require('express')
const app = express()
app.use(stHttpLoggerMiddleware)

We just call stMonitor.start() to start monitoring with Sematext when our app starts.

Other tools like Scout also shows us the memory usage of each line of code.

We can see which ones are using more memory and the times that uses more memory.

These can also monitor each request for performance issues.

JavaScript

We can improve our Express app’s JavaScript code to make it easier to test and maintain.

Pure Functions

Pure functions are functions that let returns something and don’t change any outer state.

If we pass in the same parameters to them, then they’ll always return the same value.

This makes their behavior predictable and simplifies everyone’s lives.

We can create new objects instead of mutating existing objects with pie functions.

Some examples of pure functions in JavaScript include the array instance’s map and filter methods and many more.

Object Parameters

To make working with parameters, we should reduce the number of parameters in our function.

One easy way to do it is to add an object parameter to our function.

Then we can use destructuring to destructure the properties into variables.

This way, we won’t have to worry about the order of the parameters.

For example, we can write:

const foo = ({ a, b, c }) => {
  const sum = a + b + c;
  return sum;
}

We just take the a , b , and c parameters and use them as variables.

Write Tests

Tests are great for catching regressions.

This way, if we change our code, we can have peace of mind that we didn’t kill any existing code if the existing tests pass.

With JavaScript, there are many test frameworks, including Mocha, Chai, Jasmine, and Jest.

We can use any of them to run test.

With Chai, we can write:

const chai = require('chai')
const expect = chai.expect

const foo = require('./src/foo')

describe('foo', function () {
  it('should be a function', function () {
    expect(foo).to.be.a('function')
  })
})

to import the foo file and run tests on it with Chai.

Conclusion

We should handle exceptions properly.

Also, we can use tests to prevent errors.

Pure functions and object parameters also help us write cleaner code.

Categories
Node.js Best Practices

Node.js Best Practices — Environment and Clusters

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.

Categories
Node.js Best Practices

Node.js Best Practices — Data and Security

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.

Enable TLS/SSL

This is the most basic security best practice.

We don’t anyone snooping at the data that is being sent and received.

It’s also important because it can ensure that the data isn’t modified during transit.

We can install a SSL certificate with any web host.

Let’s Encrypt provides us with free SSL certificates that can be renewed automatically.

Also, our Node app shouldn’t be exposed directly to the Internet even though we’re communicating through SSL.

For example, we can write:

app.set(‘trust proxy’, ‘1.0.0.0’);

to let traffic from our proxy to our app.

Then we can use this in addition to a reverse proxy like Nginx to put our app being a reverse proxy.

The proxy should also set the X-Forwarded-Proto: https HTTP header.

Test HTTPS Certificate Transmission

We can test HTTPS certificate transmission with Qualys SSL Labs, nmap, OpenSSL or sslyze.

We can run nmap by running:

nmap --script ss-cert,ssl-enum-ciphers -p 443 example.com

We test SSL transmission with these options.

example.com is our site.

443 is the port that SSL communicates through.

With sslyze, we run:

sslyze.py --regular example.com:4444

With the OpenSSL client, we run:

echo ‘q’ | openssl s_client -host example.com -port 443

Check for Known Security Vulnerabilities

We should keep up to date with the latest known security vulnerabilities.

We can check our dependencies form them with a few tools. They include websites like Snyk, Node Security Project, and Retire.js.

Encode All Untrusted Data Sent to an Application

To keep untrusted data from running anything malicious, we should sanitize them so that they can’t be run as code.

There’re a few ways to encode them.

HTML encoding can be done with the escape-html package on the back end.

Anything that’s within a tag is escaped.

CSS Encoding can be done with the css.escape library.

JavaScript encoding can be done with the js-string-escape library, which works on both the browser and with Node.

To escape URLs, we can use the encodeURIComponent function escape them on the front end.

And urlencode can be used on the back end.

Prevent Parameter Pollution to Stop Possible Uncaught Exceptions

We should prevent parameter pollution which can generate possible exceptions.

If we have unexpected data types for the parsed query string, then we may get exceptions when they’re parsed and we try to do something with them.

For instance, if we have the given endpoint:

app.get('/foo', function(req, res){
  if(req.query.name){
    res.status(200).send(req.query.name.toUpperCase())
  } else {
    res.status(200).send('Hi');
  }
});

Then if we make the query to:

http://example.com:8080/foo?name=james&name=mary

Then req.query.name will be an array since we have 2 values with the key name .

Array has no toUpperCase method so we’ll have an error.

Therefore, we should check for that so that we can stop the toUpperCase method from being called if req.query.name is an array.

Conclusion

We should enable SSL/TLS with any app. We can get an SSL certificate for free.

Also, we should check our data and escape them.

And we should check for security vulnerabilities.

Categories
Node.js Best Practices

Node.js Best Practices — Caching and REST

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.

Enabling Caching with Redis

We can enable caching with Redis to speed up our Express app.

To do this, we can install Redis by running:

apt update
apt install redis-server

Then in /etc/redis/redis.conf we change:

supervised no

to:

supervised systemd

Then Redis will run under Systemd.

Then we restart Redis to make the change take effect:

systemctl restart redis
systemctl status redis

Then we install the redis NPM module by running:

npm i redis

Then we can use it by writing:

const express = require('express')
const app = express()
const redis = require('redis')
​
const redisClient = redis.createClient(6379)
​
async function getData(req, res, next) {
  try {
    //...
    redisClient.setex(id, 3600, JSON.stringify(data))
    res.status(200).send(data)
  } catch (err) {
    console.error(err)
    res.status(500)
  }
}
​
function cache (req, res, next) {
  const { id } = req.params
​
  redisClient.get(id, (err, data) => {
    if (err) {
      return res.status(500).send(err)
    }
    if (data !== null) {
      return res.status(200).send(data)
    }
    next()
  })
}
​
​
app.get('/data/:id', cache, getData)
app.listen(3000, () => console.log(`Server running on Port ${port}`))

We use the Redis client for Node apps to create the Redis client.

Then we create the cache middleware that gets the data from the Redis cache if it exists.

We send the response data from Redis if it exists.

If it’s not, then we call our getData route middleware to get the data from the database.

This is done with the redisClient.setex method to set the data.

Enable VM/Server-Wide Monitoring and Logging

We can enable server or virtual machine monitoring and logging with various tools.

This way, we can watch for any issues that arise from the app.

The Hidden Powers of NODE_ENV

NODE_ENV can make a big difference in the performance of our Node app.

If we set it to production , caching is enabled so that data will be cached.

We don’t want that in development since we always want to see the latest data.

Views are cached in production but not in development node.

We can run it with the given NODE_ENV with:

NODE_ENV=<environment> node server.js

where server.js is the entry point of our app.

With production mode on, Express apps aren’t busy processing Pug templates all the time.

The CPU is free to do other things because of caching.

We can set the NODE_ENV by running:

export NODE_ENV=production

in Linux and Mac OS.

In Windows, we can run:

SET NODE_ENV=production

We can run:

NODE_ENV=production node my-app.js

in all platforms.

Use HTTP Methods and API Routes

We should use HTTP methods and API routes that matches REST conventions.

For example, we can write:

  • POST /article or PUT /article:/id to create a new article
  • GET /article to retrieve a list of article
  • GET /article/:id to retrieve an article
  • PATCH /article/:id to modify an existing articlerecord
  • DELETE /article/:id to remove an article

If we get by ID, we have the ID parameter at the end.

Conclusion

Caching and setting NODE_ENV to production speed up our app.

REST conventions are useful for keeping our APIs consistent.

Categories
Node.js Best Practices

Node.js Best Practices — Automation

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.

Using APM products

APM products let us discover performance issues with our apps.

It goes beyond traditional monitoring and measures the experience of users.

It can highlight the root cause of the problems in our app.

Downtimes can also be measured.

Create a Maintenance Endpoint

We can create a maintenance endpoint to show us the health of the app.

Also, we can use it for monitoring our app.

We don’t want to experiment with our app to find our data about them.

Security

There’re many obvious security-related things we should think about.

For instance, we can use VPNs to access things that shouldn’t be exposed to the public.

Also, we should secure business transactions with SSL/TLS.

SQL injection should also be avoided with stored procedures and parameterized queries.

HTTP headers and cookies that expose too much data about the internals of our system should also be avoided.

Move Frontend Assets Out of Node

Frontend assets should be moved out of our Node app since serving front end assets will bog down our app.

The single-threaded model will make this a burden.

If we serve assets from the front end, the Node thread will remain busy streaming files to users.

It won’t have much capacity to produce dynamic content.

Kill Servers Almost Every Day

Servers should be killed almost every day.

To do this, we need to use an external data store to store our app’s state.

We can’t have a local app state which we rely on.

Killing servers free up resources regularly.

Measure and Guard the Memory Usage

We should look for memory leaks in our apps.

This way, our app would free up memory for other processes.

We’ve to monitor it so that our app won’t be leaking megabytes of memory.

The max amount of memory is 1.5GB for a single Node app instance, so it’s a good idea to be efficient.

Assign Transaction ID to Each Log Statement

Logging with transaction ID lets us trace the workflow our app went through.

Because of the async nature of Node apps, this is especially important.

With th IDs, we can trace our app’s activities easier since it’s not async.

Tools that Automatically Detect Vulnerabilities

Vulnerabilities let attackers attack our app.

Security holes are fixed regularly for maintained dependencies, so we should update them often so that we can patch them.

If we see threats, we should watch for them until they’re fixed.

It’s easy to automate this with various tools like Dependabot.

Automated, Atomic and Zero-Downtime Deployments

Automated, atomic, and zero-downtime deployments are important.

This reduces risks for every deploy so we can deploy more often.

If it can be done with a click of a button, then we won’t be stressed when we’re doing it.

Atomic deployments make them easily reversible in case anything goes wrong.

We can do this easily with Docker and CI tools.

They have turned into the industry standard.

Conclusion

We can use some tools to help us with monitoring and deployment.

Also, it’s important to reduce the burden of our Node app since it’s single-threaded.