tutorial // Mar 11, 2022

How to Schedule and Run Cron Jobs in Node.js

How to write cron jobs using crontab statements and schedule them with the `node-cron` package.

How to Schedule and Run Cron Jobs in Node.js

Getting Started

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start. Before you do, we need to install one dependency: node-cron.

Terminal

npm i node-cron

After that's installed, go ahead an start up your server:

Terminal

cd app && joystick start

After this, your app should be running and we're ready to get started.

What is a cron job?

A cron job or "chronological job" (taken from the name of the original crontab tool that invented the concept of a cron job) is an automated task that runs at a specific time or on a specific interval. For example, in the physical world, you may wake up every day and follow a routine like:

  1. Take a shower (6:00 AM)
  2. Brush your teeth (6:15 AM)
  3. Get dressed (6:30 AM)
  4. Eat breakfast (6:40 AM)

Each part of that routine is a "job." Every single day, you "complete" or "run" that job. More likely than not, you do these same things at roughly the same time every day.

Similar to this, in an app, you may have some task that needs to be performed every day or at a specific time, for example:

  1. Send an email of the previous day's traffic, every day at 12:00am.
  2. Every three hours, clear temporary data out of a database table/collection.
  3. Once per week, fetch the latest price list from a vendor's API.

Each of these are jobs that need to be performed in our app. Because we don't want to run those manually (or have to remember to run them), we can write a cron job in our code that does it automatically for us.

Cron jobs can be schedule in one of two ways: automatically when we start up our application, or, on-demand via a function call.

Wiring up a cron job

Fortunately, cron jobs are simple in nature. They consist of two key parts:

  1. A crontab statement which describes when a job should run.
  2. A function to call when the current time matches the crontab statement.

To begin, we're going to write a function that can run multiple cron jobs for us and then see how to wire up each individual job:

/api/cron/index.js

export default () => {
  // We'll write our cron jobs here...
}

Nothing much here, just a plain arrow function. Our goal will be to define our cron jobs inside of this function and then call this function when our app server starts up. This is intentional because want to make sure our app is up and running before we schedule any cron jobs (to avoid hiccups and make sure code our jobs depend on is available).

Real quick, let's see how we're going to call this on server start up:

/index.server.js

import node from "@joystick.js/node";
import api from "./api";
import cron from './api/cron';

node.app({
  api,
  routes: {
    "/": (req, res) => { ... },
}).then(() => {
  cron();
});

In the index.server.js file here (created for us when we ran joystick create above), we've made a small change.

On the end of the call to node.app()—the function that starts up our app in Joystick—we've added a .then() callback. We're using this because we expect node.app() to return us a JavaScript Promise. Here, .then() is saying "after node.app() has run and resolved, call this function."

In this code, "this function" is the function we're passing to .then(). This function gets called immediately after node.app() resolves (meaning, the JavaScript Promise has signaled that its work is complete and our code can continue).

At the top of our file, we've imported our cron() function that we spec'd out in /api/cron/index.js. Inside of our .then() callback, we call this function to start our cron jobs after the server starts up.

/api/cron/index.js

import cron from 'node-cron';
import { EVERY_30_SECONDS, EVERY_MINUTE, EVERY_30_MINUTES, EVERY_HOUR } from './scheduleConstants';

export default () => {
  cron.schedule(EVERY_30_SECONDS, () => {
    // We'll do some work here...
  });

  cron.schedule(EVERY_MINUTE, () => {
    // We'll do some work here...
  });

  cron.schedule(EVERY_30_MINUTES, () => {
    // We'll do some work here...
  });

  cron.schedule(EVERY_HOUR, () => {
    // We'll do some work here...
  });
}

Back in our /api/cron/index.js file we filled out our function a bit. Now, up top, we can see that we've imported the cron object from the node-cron package we installed earlier.

Down in our exported function, we call the cron.schedule() function which takes two arguments:

  1. The crontab statement defining the schedule for the cron job.
  2. A function to call when the time specified by the schedule occurs.

Up at the top of our file, we can see some named variables being imported from a file that we need to create in the /api/cron folder: scheduleConstants.js.

/api/cron/scheduleConstants.js

// NOTE: These can be easily generated with https://crontabkit.com/crontab-expression-generator

export const EVERY_30_SECONDS = '*/30 * * * * *';
export const EVERY_MINUTE = '* * * * * ';
export const EVERY_30_MINUTES = '*/30 * * * *';
export const EVERY_HOUR = '0 0 * * * *';

Here, we have four different crontab statements, each specifying a different schedule. To make things easier to understand in our code, in this file, we're assigning a human-friendly name to each statement so that we can quickly interpret the schedule in our code.

Crontab statements have a unique syntax involving asterisks (or "stars," if you prefer) where each star represents some unit of time. In order, from left to right, the stars stand for:

  1. Minute
  2. Second
  3. Hour
  4. Day of the month
  5. Month
  6. Day of the week

As we see above, each star can be replaced with numbers and characters to specify certain intervals of time. This is a big topic, so if you're curious about the inner workings of crontab itself, it's recommended that you read this guide.

/api/cron/index.js

import cron from 'node-cron';
import fs from 'fs';
import { EVERY_30_SECONDS, EVERY_MINUTE, EVERY_30_MINUTES, EVERY_HOUR } from './scheduleConstants';

const generateReport = (interval = '') => {
  if (!fs.existsSync('reports')) {
    fs.mkdirSync('reports');
  }

  const existingReports = fs.readdirSync('reports');
  const reportsOfType = existingReports?.filter((existingReport) => existingReport.includes(interval));
  fs.writeFileSync(`reports/${interval}_${new Date().toISOString()}.txt`, `Existing Reports: ${reportsOfType?.length}`);
};

export default () => {
  cron.schedule(EVERY_30_SECONDS, () => {
    generateReport('thirty-seconds');
  });

  cron.schedule(EVERY_MINUTE, () => {
    generateReport('minute');
  });

  cron.schedule(EVERY_30_MINUTES, () => {
    generateReport('thirty-minutes');
  });

  cron.schedule(EVERY_HOUR, () => {
    generateReport('hour');
  });
}

Back in our code, now we're ready to put our cron jobs to use. Like we saw before, we're importing our named crontab statements from /api/cron/scheduleConstants.js and passing them as the first argument to cron.schedule().

Now, we're ready to do some actual work...or at least, some fake work.

Up above our exported function and just below our imports, we've added a function generateReport() to simulate the work of "generating a report" on some interval. That function takes in an arbitrary interval name and attempts to create a file in the reports directory of our app. Each file's name takes the shape of <interval>_<timestamp>.txt where <interval> is the interval name we pass into the generateReport() function and <timestamp> is the ISO-8601 date string marking when the file was created.

To get there, first, we make sure that the reports directory actually exists (required as we'll get an error if we try to write a file to a non-existent location). To do that, up top, we've imported fs from the fs package—a core Node.js package used for interacting with the file system.

From that package, we use fs.existsSync() to see if the reports directory exists. If it doesn't, we go ahead and create it.

If it does exist, next, we read the current contents of the directory (an array list of all the files inside of the directory) as existingReports and then take that list and filter it by interval type using the JavaScript Array.filter function.

With all of this, we attempt to write our file using the <interval>_<timestamp>.txt pattern we described above as the file name, and setting the content of that file equal to a string that reads Existing Reports: <count> where <count> is equal to the existing number of reports of interval type at the time of generation (e.g., for the first report it's 0, for the next it's 1, and so on).

That's it! Now, when we start up our server, we should see our cron jobs running and reports showing up in the /reports directory.

Wrapping up

In this tutorial, we learned how to write and schedule cron jobs in Node.js using the node-cron package. We learned how to organize our cron job code and make sure to call it after our app starts up. We also learned how crontab statements work and how to write multiple cron jobs using pre-written constants which make our crontab statements easier to understand.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode