DEV Community

Cover image for Scheduled Cron Jobs with Render
Alvin Lee
Alvin Lee

Posted on • Originally published at dzone.com

Scheduled Cron Jobs with Render

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.

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. 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 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 on your development machine. In our demo code, we’ll use Yarn 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.

In Render, we create a web service page that uses the forked repo.

Image description

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:

~/project$ yarn add pg express-async-handler node-mailjet
Enter fullscreen mode Exit fullscreen mode

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 on Render.

Image description

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 in your local project directory. We'll use a connection pool 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:

const { Pool } = require('pg');
const pool = new Pool(
  { connectionString: '<External Connection String>?ssl=true'}
);
Enter fullscreen mode Exit fullscreen mode

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:

 pool.query(
  'CREATE TABLE notes (text text, created timestamp);',
  console.log
);
Enter fullscreen mode Exit fullscreen mode

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

Next, click on New Environment Group.

Image description

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:

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

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

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

const { Pool } = require('pg');
Enter fullscreen mode Exit fullscreen mode

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

const asyncHandler = require('express-async-handler');
Enter fullscreen mode Exit fullscreen mode

We instantiate our Pool with the CONNECTION_STRING environment variable:

const connectionString = process.env.CONNECTION_STRING;
const pool = new Pool({connectionString});
Enter fullscreen mode Exit fullscreen mode

Since we're expecting a JSON POST request, let's also use JSON middleware from Express, which will parse the request body into a JavaScript object that we can access at req.body:

app.use(express.json());
Enter fullscreen mode Exit fullscreen mode

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.

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

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:

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);
}));
Enter fullscreen mode Exit fullscreen mode

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. 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:

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

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.

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:

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});
Enter fullscreen mode Exit fullscreen mode

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, which will allow us to use the await keyword to make it easier to work with:

(async () => {
  // all remaining code will go here
})();
Enter fullscreen mode Exit fullscreen mode

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:

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);
Enter fullscreen mode Exit fullscreen mode

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

if (result.rows.length === 0) {
  console.log('No latest notes');
  process.exit();
}
Enter fullscreen mode Exit fullscreen mode

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:

const emailMessage = result.rows.map(note => note.text).join('<br>');
Enter fullscreen mode Exit fullscreen mode

Finally, we use the Mailjet client to send an 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:

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);
Enter fullscreen mode Exit fullscreen mode

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

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!

Top comments (0)