Adding a PostgreSQL Database to a Node.js App on Heroku

Introduction

Heroku is a hosting service that supports Node.js applications. It is easy to use and its functionality can be extended with add-ons. There are add-ons for various things, including messaging/queues, logging, metrics, and of course, data stores. The data store add-ons support popular databases, like PostgreSQL, Redis, and DynamoDB.

In this tutorial we'll be adding a PostgreSQL database to a Node application that shortens URLs. We will then deploy the application to Heroku and set up the PostgreSQL add-on.

PostgreSQL

If you don't have it already, you'll need to install Postgres on your machine. There are a few different ways to install it, depending on your OS. Visit the PostgreSQL downloads page for more information.

With PostgreSQL installed, we can create a database for the URL shortener application to use:

$ psql
psql (11.6)
Type "help" for help.

tomkadwill=#

And then use the CREATE DATABASE SQL command:

tomkadwill=# CREATE DATABASE urlshortener_development;
CREATE DATABASE
tomkadwill=# \l
                                         List of databases
            Name          |   Owner    | Encoding |   Collate   |    Ctype    |   Access privileges
--------------------------+------------+----------+-------------+-------------+-----------------------
 urlshortener_development | tomkadwill | UTF8     | en_US.UTF-8 | en_US.UTF-8 |

Here we create a database called urlshortener_development and then use \l to print a list of all PostgreSQL databases in the system.

Our new database urlshortener_development is there so we know it was created successfully. Also, note the database owner because we'll need it later on (yours will be different from mine).

Integrating Postgres into a Node App

The Node application that we will be working on is fairly simple. If you want to build it from scratch you can follow our guide, Deploying a Node.js App to Heroku, or you can download it from GitHub.

The Express app logic is all inside app.js:

const express = require('express');
const app = express();
const path = require('path');
const port = process.env.PORT || 3000;
const urlShortener = require('node-url-shortener');

const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({extended: true}));
app.use(express.urlencoded());

app.get('/', function(req, res) {
  res.sendFile(path.join(__dirname + '/index.html'));
});

app.post('/url', function(req, res) {
  const url = req.body.url

  urlShortener.short(url, function(err, shortUrl){
    res.send(shortUrl);
  });
});

app.listen(port, () => console.log(`url-shortener listening on port ${port}!`));

You can run the application via npm start. Once started, browse to localhost:3000 and you should see the homepage:

app_homepage

The plan is to update app.js so that it stores each URL and shortened URL in a DB table and then display the last 5 results on the UI.

The first thing we need to do is install an ORM (Object Relation Mapper) library. Interacting directly with PostgreSQL is difficult because we'd have to write our own raw SQL queries.

An ORM allows us to interact with the database via simpler API calls. Note that there are some downsides to using ORMs, but I won't cover them in this tutorial.

There are a number of different ORM libraries for Node, in this case we'll be using Sequelize:

$ npm install --save sequelize
$ npm install --save pg pg-hstore

The first command installs Sequelize and the second installs the PostgreSQL driver for Node. Sequelize supports multiple databases so we need to specify which one to use and provide the Node driver.

Migrations

With Sequelize installed and configured, we can think about the database structure. We only need something simple, a single table with 3 columns: a unique ID, original URL, and shortened URL.

We could create the new database table manually, but that would make deployments painful. We'd have to remember our queries and run them on each environment.

A better way to handle database changes is via migrations, which is where database changes are codified inside the application. Luckily, Sequelize supports migrations out of the box. Let's write a migration to create a table for URLs.

First, we'll install the Sequelize CLI, which allows us to run migrations:

$ npm install --save sequelize-cli

Next we'll initialize Sequelize:

$ npx sequelize-cli init

This will create a config/config.json file and models, migrations, and seeders directories.

After that, we need to modify the config.json file so that it can connect to our PostgreSQL database:

{
  "development": {
    "username": "tomkadwill",
    "password": "password",
    "database": "urlshortener_development",
    "host": "localhost",
    "dialect": "postgres",
    "operatorsAliases": false
  }
}

Once the file is done, let's generate the migration using the Sequelize CLI. Here, we'll define our fields through the attributes flag. We won't be including the id field since it's automatically added:

$ npx sequelize-cli model:generate --name Url --attributes url:string,shortUrl:string

This will create a migration file that looks something like this:

'use strict';
module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('Urls', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      url: {
        type: Sequelize.STRING
      },
      shortUrl: {
        type: Sequelize.STRING
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('Urls');
  }
};

Migrations contain an up and down function. up is used to move the database forward and down is used to roll back.

In this case up creates a Urls table that has 5 fields. It has the url and shortUrl fields, as well as id, createdAt, and updatedAt, which are added by default.

The down migration will simply drop the Urls table.

Finally, let's run the migration:

$ npx sequelize-cli db:migrate

Once that's run we can query the database directly to check that everything worked:

$ psql -p 5432 "urlshortener_development"
psql (11.6)
Type "help" for help.

urlshortener_development=# \dt
              List of relations
 Schema |     Name      | Type  |   Owner
--------+---------------+-------+------------
 public | SequelizeMeta | table | tomkadwill
 public | Urls          | table | tomkadwill
(2 rows)

urlshortener_development=# \d "Urls"
                                       Table "public.Urls"
  Column   |           Type           | Collation | Nullable |              Default
-----------+--------------------------+-----------+----------+------------------------------------
 id        | integer                  |           | not null | nextval('"Urls_id_seq"'::regclass)
 url       | character varying(255)   |           |          |
 shortUrl  | character varying(255)   |           |          |
 createdAt | timestamp with time zone |           | not null |
 updatedAt | timestamp with time zone |           | not null |
Indexes:
    "Urls_pkey" PRIMARY KEY, btree (id)

As you can see there are two database tables: SequelizeMeta and Urls. And if we inspect Urls, the expected fields are there.

Saving URLs

Now that we have PostgreSQL configured and we have created a database table with the correct structure, the next step is to update our application so that it persists URLs to the database. Recall that npx sequelize-cli model:generate created a model file, we'll use that to save URLs to the database.

First we need to import the models into app.js, by requiring models/index.js:

const db = require('./models/index.js');

The models/index.js file was generated by Sequelize and its purpose is to include all of the model files.

Next, we need to update the post route so that it creates a database record. We can use the findOrCreate() function so that each URL only has one unique database entry:

app.post('/url', function(req, res) {
  const url = req.body.url

  urlShortener.short(url, function(err, shortUrl) {
    db.Url.findOrCreate({where: {url: url, shortUrl: shortUrl}})
    .then(([urlObj, created]) => {
      res.send(shortUrl)
    });
  });
});

When db.Url.findOrCreate() is called, it will try to find a record that matches the user-provided url and generated shortUrl. If one is found then Sequelize does nothing, otherwise, it creates a new record.

Displaying URLs in the UI

For user friendliness, let's update the app to display the last 5 persisted URLs.

To that end, we'll add a template engine so that Express can render the URLs dynamically. There are many template engines available but in this case we'll use Express Handlebars:

$ npm install --save express-handlebars

After installing the package we can add it to app.js:

const exphbs = require('express-handlebars');

app.engine('handlebars', exphbs());
app.set('view engine', 'handlebars');

Next, we need to change the application directory structure. express-handlebars assumes that views are located in a views directory. It also assumes that we have a views/layouts/main.handlebars file:

.
├── app.js
└── views
    ├── index.handlebars
    └── layouts
        └── main.handlebars

So, let's move and rename the index.html file:

$ mv index.html views/index.handlebars

And finally, let's make a layout file, views/layouts/main.handlebars:

<html>
<head>
    <title>Url Shortener</title>
</head>
<body>
    {{{body}}}
</body>
</html>

There's a certain order in which the template files load: express-handlebars will render views/layouts/main.handlebars which then renders views/index.handlebars inside the {{{body}}} tag.

Now that we have the correct directory structure, let's add some HTML code to index.handlebars to dynamically display the URLs:

<ul>
  {{#each urlObjs}}
  <li>{{this.url}} -- <b>{{this.shortUrl}}</b></li>
  {{/each}}
</ul>

Here we use Handlebars' each helper to iterate over each URL object and display the original URL and short URL.

The last thing we need to do is update the GET route in app.js to pass the URLs into the view:

app.get('/', function(req, res) {
  db.Url.findAll({order: [['createdAt', 'DESC']], limit: 5})
  .then(urlObjs => {
    res.render('index', {
      urlObjs: urlObjs
    });
  });
});

Let's walk through what's happening here. When / is requested, Sequelize queries the Urls table. The query is ordered by createdAt and limited to 5, which ensures that only the 5 most recent results are returned.

The result of the query is passed into res.render as the local variable urlObjs, which will be used by the template.

The rendered page looks like this:

app_homepage_with_db

If you have any issues, you can download the completed code from GitHub.

Deploying the App to Heroku

Now that we have the application running locally, this section will cover how to get it running on Heroku and how to connect the database to it once it is running.

First, let's deploy all of our changes to Heroku:

$ git push heroku master

See Deploying a Node.js App to Heroku for a detailed guide on deploying to Heroku.

Connecting the Database

Adding a database isn't hard, and all it requires is a single command line:

$ heroku addons:create heroku-postgresql:hobby-dev

This command creates the PostgreSQL add-on for Heroku and sets an environment variable called DATABASE_URL - we just need to tell Sequelize to use it by updating the config file:

{
  "development": {
    "username": "tomkadwill",
    "password": "password",
    "database": "urlshortener_development",
    "host": "localhost",
    "dialect": "postgres",
    "operatorsAliases": false
  },
  "production": {
    "username": "tomkadwill",
    "password": "password",
    "database": "urlshortener_production",
    "host": "localhost",
    "dialect": "postgres",
    "operatorsAliases": false,
    "use_env_variable": "DATABASE_URL"
  }
}

Here we've added a new config section for production, it's the same as development except for the database name and use_env_variable field.

Sequelize uses use_env_variable to fetch the environment variable name to use for connecting to the database.

Heroku has NODE_ENV set to "production" by default, which means it will look for the production configurations. Let's commit and push to Heroku.

Next, we need to run the migrations on Heroku using sequelize db:migrate:

$ heroku run bash
Running bash on ⬢ nameful-wolf-12818... up, run.5074 (Free)
~ $ sequelize db:migrate

If we haven't created the migration before, we'd be running scripts manually now.

At this point, the application should be working in the browser.

app_homepage_with_db_on_heroku

Managing the Heroku Database

You will probably need to query or modify your production database at some point. For example, you may need to query some data to help diagnose a bug or you may need to modify some user data. Let's look at how to do that.

Running Sequelize Migration Tasks

The first thing you need to know is how to run Sequelize migrations on Heroku. This is how to view a list of available commands:

$ heroku run bash
Running bash on ⬢ nameful-wolf-12818... up, run.1435 (Free)
~ $ sequelize

These are some of the most useful:

  • sequelize db:migrate:undo: Rollback a single migration
  • sequelize db:migrate:undo:all: Rollback all migrations. Effectively revert back to a clean database
  • sequelize db:migrate:status: Check which migration your application is on
Using Sequelize Models in the Console

In addition to the CLI tasks, we can use Sequelize models directly in the Node console:

$ heroku run bash
~ $ node
Welcome to Node.js v12.14.0.
Type ".help" for more information.
>

Here are some code examples that can be run in the Node console. You may notice that they bear resemblance to the Node REPL.

Querying for an individual URL by ID:

db.Url.findByPk(1).then(url => {
  console.log(
    url.get({plain: true})
  );
});

Querying for all urls:

db.Url.findAll().then(urls => {
  urls.map(url => {
    console.log(
      url.get({plain: true})
    );
  });
});

Inserting a URL record:

db.Url.create({url: 'https://stackabuse.com/deploying-a-node-js-app-to-heroku', shortUrl: 'https://is.gd/56bEH3'});

Querying for a URL by ID and updating it:

db.Url.findByPk(1).then(url => {
  url.shortUrl = 'example.com';
  url.save();
});

Querying for a URL by ID and deleting it:

db.Url.findByPk(1).then(url => {
  url.destroy();
});

Conclusion

There are numerous add-ons that can be used to extend Heroku. One of those add-ons is Heroku Postgres, which allows you to easily set up a database to store application data.

We've extended a simple Node and Express application so that it stores urls in a Postgres database, on Heroku.