Scheduled Cron Jobs with Render
Tags: javascript, node, tutorial, showdev
Published: 2022-04-05 17:05:39

Programmers often need to run some recurring process automatically at fixed intervals or at specific times. A common solution for this problem is to use a cron job. When you have full access to your own server, configuring cron jobs is quite straightforward. However, how hard is it to configure cron jobs when you use an application hosting service? Some services, thankfully, provide a way for you to do this.

In this article, we’ll walk through a sample mini-project that shows how to easily set up and deploy a cron job on [Render](https://render.com/).

# Core Concepts

## What Is a Cron Job?

A cron job is a Unix command that `cron` runs as a background process on a schedule determined by a [Cron Expression](https://en.wikipedia.org/wiki/Cron#CRON_expression). Generally, cron determines the jobs to run via crontab configuration files, which consist of pairs of cron expressions and corresponding commands.

## What Is Render?

Render is a cloud application hosting service that offers a variety of web service hosting solutions, such as static sites, web servers, databases, and, yes, even cron jobs! Render handles the hassle of hosting and deployment for you so that you can spend all of your time focusing on building out your projects.

## What Are Render Cron Jobs?

Render offers a cron job hosting service that simplifies the process of deploying and maintaining a cron job in the cloud. To set up a Render cron job service, simply link a GitHub repo, choose a runtime, and provide the command to run and the cron expression to determine the schedule.

# Overview of Our Mini-Project

Our project will be a simple service that lets us create and store notes. The service also runs an hourly cron job to email us all the notes created in the last hour. The application consists of three parts:

* An Express web server that handles requests to create the notes
* A PostgreSQL database to store the notes
* A cron job that sends the notes digest email

We'll use Render services for each of these components. We'll also use [Mailjet](https://www.mailjet.com/) as the service for sending out emails. For our Node.js application, we’ll add the following dependency packages:

* `pg` to interact with the database
* `express-async-handler` as a quality-of-life upgrade that allows us to use async functions as our Express handlers
* `node-mailjet`, which is the official client library that interacts with the Mailjet API

We’ll assume that you have [Node.js installed](https://nodejs.org/en/download/) on your development machine. In our demo code, we’ll use [Yarn](https://yarnpkg.com/) for our package manager.

# Setting Up the Project Repo

Let's start by setting up our project repo and our web service on Render. We can fork Render's Express Hello World repo for our initial [Express server boilerplate code](https://github.com/render-examples/express-hello-world).

In Render, we [create a web service page](https://dashboard.render.com/web/new) that uses the forked repo.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/021ypw53u6n88p7rf2yy.png)

We enter a name for our web service, and we proceed with all of the default values. After Render finishes deploying, we see a service URL. We can visit that URL in our browser to verify that everything was set up correctly.

Now, we can clone the forked repo to our development machine, and then add our dependencies:

```bash
~/project$ yarn add pg express-async-handler node-mailjet
```

With our initial project repo set up, let’s move on to setting up our database.

# Setting Up the Database

Our database is very simple, consisting of just one table called notes. The table will have a column to store the note text and another column to store the timestamp when the note was created.

We’ll [create a PostgreSQL database service](https://dashboard.render.com/new/database) on Render.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pqpa9mnc2wxrbrs30345.png)

We provide a name for the database service and then use the default values for all other options. After creating the database, we can connect to it from our local machine and create the `notes` table. Copy the **external** connection string from the database dashboard, and then start up a `node` [REPL](https://nodejs.dev/learn/how-to-use-the-nodejs-repl) in your local project directory. We'll use a [connection pool](https://node-postgres.com/features/pooling) to make the query to our database, so we'll need to import the `Pool` class and create a `Pool` object with our external connection string:

```javascript
const { Pool } = require('pg');
const pool = new Pool(
  { connectionString: '<External Connection String>?ssl=true'}
);
```

Note that since we are connecting through SSL in the `node` REPL, we need to append `?ssl=true` to the end of the connection string. With our pool object created, we can execute the query to create the table:

```javascript
 pool.query(
  'CREATE TABLE notes (text text, created timestamp);',
  console.log
);
```

Voila! Our database is set up with our `notes` table!

# Setting Up an Environment Group in Render

Before we add the functionality to our web service to start populating the table, let's make sure that our web service has access to our database. In fact, because both our web service and cron job will need to connect to the database, we can take advantage of Render's environment groups to create a shared group of environment variables that we can use for both services. 

To do this, we'll want the **internal** connection string from the database dashboard, since both the web service and cron job will communicate with the database through Render’s internal network. Click on **Env Groups** in the main Render navigation.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i40jd6u75tfkqrhj19sg.png)

Next, click on **New Environment Group**.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l9v8gzgwntel28lrny7d.png)

Choose a name for your environment group. Then, add a new variable with a key of `CONNECTION_STRING`, and paste the internal connection string as the value (no need for `ssl=true` this time).

Once you've created the group, you can go back to the **Environments** settings for the web service. In the **Linked Environment Groups** section, you can select the environment group you just created, and click on **Link**. Now, our Node.js code can access any variables we define in this group through the global `process.env` object. We’ll see an example of this as we start to build out our Express app. Let’s do that now!

# Creating the Express App

Our Express app will only have one endpoint, `/notes`, where we'll handle `POST` and `GET` requests.

When we receive a `POST` request, we create a new note row in the database. We'll expect the `Content-Type` of the request to be `application/json` and the body to be formatted as `{"note": "<note text>"}`. We'll also note the time of the request and store that timestamp as the note's `created` value.

When we receive a `GET` request, we'll query the database for all the notes and return them as a JSON response.

Let's start by getting rid of all of the unnecessary code from our boilerplate. We only need to keep the following lines, and we change the `app.listen` callback slightly:

```javascript
const express = require('express');
const app = express();
const port = process.env.PORT || 3001;

app.listen(port, () => console.log(`Notes server listening on port ${port}!`));
```

Next, let's add all of the imports we'll need. Again we’ll use a connection `Pool` to connect to the database:

```javascript
const { Pool } = require('pg');
```

Additionally, we’ll make use of the `express-async-handler` package:

```javascript
const asyncHandler = require('express-async-handler');
```

We instantiate our `Pool` with the `CONNECTION_STRING` environment variable:

```javascript
const connectionString = process.env.CONNECTION_STRING;
const pool = new Pool({connectionString});
```

Since we're expecting a JSON `POST` request, let's also use [JSON middleware from Express](http://expressjs.com/en/api.html#express.json), which will parse the request body into a JavaScript object that we can access at `req.body`:

```javascript
app.use(express.json());
```

## Handling `GET /notes` Requests

Now we can get into the meat of our app: the request handlers. We'll start with our `GET` handler since it's a bit simpler. Let’s show the code first, and then we’ll explain what we’ve done.

```javascript
app.get('/notes', asyncHandler(async (req, res) => {
  const result = await pool.query('SELECT * FROM notes;');
  res.json({ notes: result.rows });
}));
```

First, we register an async function with `asyncHandler` at the `/notes` endpoint using `app.get`. In the body of the callback, we want to select all the notes in the database using `pool.query`. We return a JSON response with all of the rows we received from the database.

And that's all we need for the `GET` handler!

At this point, we can commit and push these changes. Render automatically builds and redeploys our updated application. We can verify that our `GET` handler works, but for now, all we see is a sad, empty notes object.

## Handling `POST /notes` Requests

Let's move on to our `POST` handler so that we can start populating our database with some notes! Our code looks like this:

```javascript
app.post('/notes', asyncHandler(async (req, res) => {
  const query = {
    text: 'INSERT INTO notes VALUES ($1, $2);',
    values: [req.body.note, new Date()],
  };
  await pool.query(query);
  res.sendStatus(200);
}));
```

First, we insert a new row into our database with our note text and creation timestamp. We get the note text from `req.body.note`, and we use `new Date()` to get the current time. The `Date` object is converted into a PostgreSQL data type through our use of [parameterized queries](https://node-postgres.com/features/queries#parameterized-query). We send the insert query, and then we return a `200` response.

## Deploy and Test

After pushing our code and having Render redeploy, we can test our server by sending some test requests. At the command line, we use `curl`:

```bash
curl -X POST <INSERT WEB SERVICE URL>/notes \
     -H 'Content-Type: application/json' \
     -d '{"note": "<INSERT NOTE TEXT>"}'
```

You can then visit the `/notes` endpoint in your browser to see all of your newly created notes! 

# Creating the Cron Job

The last component that ties our project together is the cron job. This cron job will run at the top of every hour, emailing us with all the notes created in the last hour.

## Set Up Mailjet

We’ll use Mailjet as our email delivery service. You can sign up for a free account [here](https://app.mailjet.com/signup).

You’ll need your Mailjet API key and secret key from the API key management page. Let's add these keys to the environment group we created earlier. Add the following environment variables:

* `MAILJET_APIKEY`
* `MAILJET_SECRET`
* `USER_NAME`: the name of the email recipient (your name)
* `USER_EMAIL`: the email address of the recipient (your email address)

## Implement Cron Job Script

Now let's write the script we'll run as the cron job, which we can call `mail_latest_notes.js`. Again, we'll use a `Pool` to query our database, and we'll also want to initialize our Mailjet client with our environment variables: 

```javascript
const { Pool } = require('pg');
const mailjet = require ('node-mailjet')
  .connect(process.env.MAILJET_APIKEY, process.env.MAILJET_SECRET);
const connectionString = process.env.CONNECTION_STRING;
const pool = new Pool({connectionString});
```

Next, let's query the database for all notes created in the last hour. Since this will be an asynchronous operation, we can wrap the rest of the script in an [async IIFE](https://developer.mozilla.org/en-US/docs/Glossary/IIFE), which will allow us to use the `await` keyword to make it easier to work with:

```javascript
(async () => {
  // all remaining code will go here
})();
```

We use another parameterized query with `new Date()` to capture the current time and use it to filter the notes. This time, however, we'll want to get the time an hour before the current time, which we can do using the `setHours` and `getHours` Date methods, so that we can filter for all the notes after that timestamp:

```javascript
const timestamp = new Date();
timestamp.setHours(timestamp.getHours() - 1);
const query = {
  text: 'SELECT * FROM notes WHERE created >= $1;',
  values: [timestamp],
};
const result = await pool.query(query);
```

We check how many rows were returned, and we won’t send the email if there aren’t any notes to send.

```javascript
if (result.rows.length === 0) {
  console.log('No latest notes');
  process.exit();
}
```

If there are rows, then we create the email message with the retrieved notes. We pull out the text from each note row with a `map` and use HTML for some easy formatting, joining all the note texts with `<br>` tags:

```javascript
const emailMessage = result.rows.map(note => note.text).join('<br>');
```

Finally, we use the Mailjet client to [send an email](https://dev.mailjet.com/email/guides/send-api-v31/#send-a-basic-email) with the message we just created and the environment variables we set up earlier. We can also log the response we get back from Mailjet, just to make sure that our email was sent:

```javascript
const mailjetResponse = mailjet
  .post('send', {'version': 'v3.1'})
  .request({
    'Messages':[{
      'From': {
        'Email': process.env.USER_EMAIL,
        'Name': process.env.USER_NAME
      },
      'To': [{
        'Email': process.env.USER_EMAIL,
        'Name': process.env.USER_NAME
      }],
      'Subject': 'Latest Notes',
      'HTMLPart': `<p>${emailMessage}</p>`
    }]
  });

console.log(mailjetResponse);
```

That's all we need for our script! 

# Set Up Render Cron Job Service

Lastly, let's create the cron job service on Render.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s8i836mzwlhfrpxt9kpi.png)

We give our cron job service a name and set the environment to `Node`. Then, we set the command field to `node mail_latest_notes.js`. To run the script every hour, we set the schedule field to the cron expression `0 * * * *`. Render has a nifty label under the input which shows what the cron expression translates to in plain English. We create the cron job.

Next, we go to the **Environment** tab for the cron job service, and we link the environment group that we created earlier. All that's left to do is wait for Render to finish building our cron job service. Then, we can test it! Before the build finishes, you can create more notes to make sure the script sends an email. Finally, you can click on the **Trigger Run** button on the cron dashboard to manually run the script, and check your inbox to make sure you receive that email.

And with that, we've finished our notes project! 

# Conclusion

Job schedulers like `cron` are powerful tools that provide a simple interface to run automated processes on strict schedules. Some application hosting services — like Render — make it easy for you to set up cron job services alongside your web and database services. In this article, we walked through how to do just that, by building a mini-project that saves notes and then sends an email digest triggered hourly by a cron job. With Render, coordinating communication between our various components and setting up the cron job was straightforward and simple.

Happy coding!