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.
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.
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.
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.
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:
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 databaseexpress-async-handler
as a quality-of-life upgrade that allows us to use async functions as our Express handlersnode-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.
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.
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
With our initial project repo set up, let’s move on to setting up our 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.
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'}
);
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
);
Voila! Our database is set up with our notes
table!
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.
Next, click on New Environment Group.
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!
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}!`));
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');
Additionally, we’ll make use of the express-async-handler
package:
const asyncHandler = require('express-async-handler');
We instantiate our Pool
with the CONNECTION_STRING
environment variable:
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, which will parse the request body into a JavaScript object that we can access at req.body
:
app.use(express.json());
GET /notes
RequestsNow 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 });
}));
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.
POST /notes
RequestsLet'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);
}));
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.
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>"}'
You can then visit the /notes
endpoint in your browser to see all of your newly created notes!
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.
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)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});
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
})();
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);
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();
}
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>');
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);
That's all we need for our script!
Lastly, let's create the cron job service on Render.
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!
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!
Also Published Here