No Time Dad

A blog about web development written by a busy dad

Using Node.js For Scripting and Automating Tasks

Intro

I generally write my scripts in bash because my scripts are generally short and generally simple. There are some cases where I need a more complex script or I want better debugging options in my script and bash just won’t cut it anymore. In those cases I have been known to reach for Python or Node.js. Here I am going to focus on Node.js and show you how it can be used for more than just servers.

Setup

For the examples shown here you will need to have Node.js installed on your system. I am using the Linux binary since I am on a Linux machine, but you should download whichever binary works for your machine. I also highly recommend looking at Node Version Manager (nvm) for managing the Node.js versions in your environment.

For the purposes of the examples shown here:

node -v
v14.15.0

Getting Started

We’re going to create a Node.js script that checks to see if a given website is up. It won’t use any third party modules and should run anywhere you have Node.js installed.

Decided on the directory where you want the script to live (it can be anywhere) and let’s create a file called siteCheck.js and making it executable.

touch siteCheck.js
chmod +x siteCheck.js

We want our file to be executable so let’s add a shebang which signals who we want to run the script. In this case we want the script to be run by node. We will also add a simple console.log to siteCheck.js just as a sanity check that everything is working. Your file should look like this

#!/usr/bin/env node

console.log('hello');

We can now execute the script by calling it directly. We should see hello logged to the console. Note the ./ before siteCheck.js, this is how we execute scripts that are not on the PATH.

$ ./siteCheck.js
hello

Adding the Request Handler

We’ll use https module from Node.js for our requests. I am a fan using promises to control the flow of execution of the code when working Node.js, so that is what we’ll do here too. Update the siteCheck.js with the following:

#!/usr/bin/env node
var https = require('https');

function makeRequest(url) {
  return new Promise((resolve, reject) => {
    https.get(url, (res) => {
      resolve(res.statusCode);
    })
      .on('error', (errorRes) => {
        reject(errorRes);
      });
  });
}

This function returns a Promise containing an https.get request to the specified url argument. We are only concerned about the status code returned from the HTTP request, so if everything is ok with the request we’ll resolve the promise with the status code. If something goes wrong and we get an error, we will reject the promise and return the error response from the request.

Call the Request Handler

Now we need to call this function. Update your siteCheck.js file as shown below:

#!/usr/bin/env node
var https = require('https');

function makeRequest(url) {
  return new Promise((resolve, reject) => {
    https.get(url, (res) => {
      resolve(res.statusCode);
    })
      .on('error', (errorRes) => {
        reject(errorRes);
      });
  });
}

const reqUrl = 'https://stackoverflow.com/';
makeRequest(reqUrl)
  .then(statusCode => {
    if (statusCode === 200) {
      console.log(`${reqUrl} is up (${statusCode})`)
    } else {
      throw Error(`${reqUrl} is not up (${statusCode})`)
    }
  })
  .catch((err) => {
    console.log(err);
    process.exitCode = 1
  });

Here we will call makeRequest to see if https://stackoverflow.com/ is up. If the status code is 200 then we’ll just log a message saying everything is ok and exit with code 0 (which happens automatically). However, if the status code is not 200 or something else bad happened we want to log that message and exit with code 1 (or any non-zero). We’ll explicitly tell Node.js via process.exitCode that things are not ok and we want to exit non-zero.

In some cases just console logging the error is enough, but I’d suggest forcing a non-zero exit code so you can quickly identify issues and be alerted if you run your script on any type of CI/CD or third-party task service. Generally, these types of services mechanisms in place to notify you if a process exits non-zero.

The script can be ran exactly the same as before:

$ ./siteCheck.js
https://stackoverflow.com/ is up (200)

If we mangle the url in the script to one we know is bad, we can see an example of the error response after running the script

...
const reqUlr = 'https://asdfstackoverflow.com/';
...
Error: getaddrinfo ENOTFOUND asdfstackoverflow.com
    at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:67:26) {
  errno: -3008,
  code: 'ENOTFOUND',
  syscall: 'getaddrinfo',
  hostname: 'asdfstackoverflow.com'
}

It is important to note in the example above that this was not a non-200 error but instead an ENOTFOUND error. If you set a breakpoint on the if (statusCode === 200) block you would see that it never gets hit. So we are covering all of our bases here with the catch.

Reusability Improvements

The script will work fine as it is, but it would be handy if it was usable for sites other than the one we have hardcoded. Luckily, node has an easy way to help us with this. We’ll just pass the site url as an argument when we run the script. We’ll also add some basic error handling to ensure that we remember to call the script with a value. You could go down the rabbit hole of validating the url but I’d suggest for the purposes of a quick script to just leave it as-is. If you were using this logic as part of a production system then I’d say that you should definitely add improved validation, but it is not needed here.

We can access the command line arguments via process.argv, which returns an array containing the path to the node process, the path to our script, and all (if any) command line arguments. If we called our script like ./siteCheck.js hello we’d something similar to this:

[
  '/home/notimedad/.nvm/versions/node/v14.15.0/bin/node',
  '/home/notimedad/workspace/node-sample-scripts/siteCheck.js',
  'hello'
]

Looking at the above we can see that only the 3rd item in the array is important to us, so we’ll slice from there an perform some checks. We’ll update our script to include the following above the makeRequest function:

#!/usr/bin/env node
var https = require('https');

const args = process.argv.slice(2);
if (!args.length) {
  console.log('Please supply a URL as the first argument.')
  process.exitCode = 1;
  process.exit();
}
const reqUrl = args[0];
...

Here we slice and check the length of the array. If the array length is 0 after we slice the first two items then we know that we forgot to pass in the url string when we ran the script. We will log a message, set the exitCode to 1, and exit the script. When the url is passed to the script we will save it in the variable reqUrl, which has been removed further down from the previous version of the script where it was hardcoded. Now our script is more flexible and can be re-used as needed. Here is how the final script looks now:

#!/usr/bin/env node
var https = require('https');

const args = process.argv.slice(2);
if (!args.length) {
  console.log('Please supply a URL as the first argument.')
  process.exitCode = 1;
  process.exit();
}
const reqUrl = args[0];

function makeRequest(url) {
  return new Promise((resolve, reject) => {
    https.get(url, (res) => {
      resolve(res.statusCode);
    })
      .on('error', (errorRes) => {
        reject(errorRes);
      });
  });
}

makeRequest(reqUrl)
  .then(statusCode => {
    if (statusCode === 200) {
      console.log(`${reqUrl} is up (${statusCode})`)
    } else {
      throw Error(`${reqUrl} is not up (${statusCode})`)
    }
  })
  .catch((err) => {
    console.log(err);
    process.exitCode = 1
  });

Some examples:

$ ./siteCheck.js https://stackoverflow.comf
Error: getaddrinfo ENOTFOUND stackoverflow.comf
    at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:67:26) {
  errno: -3008,
  code: 'ENOTFOUND',
  syscall: 'getaddrinfo',
  hostname: 'stackoverflow.comf'
}
$ ./siteCheck.js https://stackoverflow.com
https://stackoverflow.com is up (200)

Closing Thoughts

Writing a quick status code checker in Node.js is probably overkill and something you could likely do with a single curl command on a cron, but the point was to show you that Node.js can be a powerful scripting language if you need to do more complex things or just enjoy writing JavaScript more than you enjoy writing shell scripts.