September 17, 2020

Backend with TypeScript, PostgreSQL & Prisma: CI & Deployment

In this fourth part of the series, we'll configure continuous integration (CI) and continuous deployment (CD) with GitHub Actions to test and deploy the backend to Heroku.

Backend with TypeScript, PostgreSQL & Prisma: CI & Deployment

Introduction

The goal of the series is to explore and demonstrate different patterns, problems, and architectures for a modern backend by solving a concrete problem: a grading system for online courses. This problem was chosen because it features diverse relations types and is complex enough to represent a real-world use case.

The recording of the live stream is available above and covers the same ground as this article.

What the series will cover

The series will focus on the role of the database in every aspect of backend development covering:

TopicPart
Data ModelingPart 1
CRUDPart 1
AggregationsPart 1
REST API layerPart 2
ValidationPart 2
TestingPart 2
Passwordless AuthenticationPart 3
AuthorizationPart 3
Integration with external APIsPart 3
Continuous IntegrationPart 4 (current)
DeploymentPart 4 (current)

In the first article, you designed a data model for the problem domain and wrote a seed script that uses Prisma Client to save data to the database.

In the second article of the series, you built a REST API on top of the data model and Prisma schema from the first article. You used Hapi to build the REST API, which allowed performing CRUD operations on resources via HTTP requests.

In the third article of the series, you implemented email-based passwordless authentication and authorization, using JSON Web Tokens (JWT) with Hapi to secure the REST API. Moreover, you implemented resource-based authorization to define what users are allowed to do.

What you will learn today

In this article, you will set up GitHub Actions as the CI/CD server by defining a workflow that runs the tests and deploys the backend to Heroku, where you will host the backend and the PostgreSQL database.

Heroku is a platform as a service (PaaS). In contrast to the serverless deployment model, with Heroku, your application will run constantly even if no requests are made to it. While serverless has many benefits such as lower costs and less operational overhead, this approach avoids the challenges of database connection churn and cold starts that are common to the serverless approach.

To learn more about the trade-offs between deployment paradigms for applications using Prisma, check out the Prisma deployment docs.

Note: Throughout the guide, you'll find various checkpoints that enable you to validate whether you performed the steps correctly.

Prerequisites

To deploy the backend with GitHub Action to Heroku, you will need the following:

  • Heroku account.
  • Heroku CLI installed.
  • The SendGrid API token for sending emails, which you created in part 3 of the series.

Continuous integration and continuous deployment

Continuous integration (CI) is a technique used to integrate the work from individual developers into the main code repository to catch integration bugs early and accelerate collaborative development. Typically, the CI server is connected to your Git repository, and every time a commit is pushed to the repository, the CI server will run.

Continuous deployment (CD) is an approach concerned with automating the deployment process so that changes can be deployed rapidly and consistently.

While CI and CD are concerned with different responsibilities, they are related and often handled using the same tool. In this article, you will use GitHub Actions to handle both CI and CD.

Continuous integration pipelines

With continuous integration, the main building block is a pipeline. A pipeline is a set of steps you define to ensure that no bugs or regressions are introduced with your changes. For example, a pipeline might have steps to run tests, code linters, and the TypeScript compiler. If one of the steps fails, the CI server will stop and report the failed step back to GitHub.

When working in a team where code changes are introduced using pull requests, CI servers would usually be configured to automatically run the pipeline for every pull request.

The tests you wrote in the previous steps work by simulating requests to the API's endpoints. Since the handlers for those endpoints interact with the database, you will need a PostgreSQL database with the backend's schema for the duration of the tests. In the next step, you will configure GitHub Actions to run a test database (for the duration of the CI run) and run the migrations so that the test database is in line with your Prisma schema.

Note: CI is only as good as the tests you wrote. If your test coverage is low, passing tests may create a false sense of confidence.

Defining a workflow with GitHub Actions

GitHub Actions is an automation platform that can be used for continuous integration. It provides an API for orchestrating workflows based on events in GitHub and can be used to build, test, and deploy your code from GitHub.

To configure GitHub Actions, you define workflows using yaml. Workflows can be configured to run on different repository events, e.g., when a commit is pushed to the repository or when a pull request is created.

Each workflow can contain multiple jobs, and each job defines multiple steps. Each step of a job is a command and has access to the source code at the specific commit being tested.

Note: CI services use different terms for pipeline; for example, GitHub Actions uses the term workflow to refer to the same thing.

In this article, you will use the grading-app workflow in the repository.

Let's take a look at the workflow:

The grading-app workflow has two jobs: test and deploy.

The test job will do the following:

  1. Check out the repository.
  2. Configure node.
  3. Install the dependencies.
  4. Create the database schema in the test database that is started using services.
  5. Run the tests.

Note: services can be used to run additional services. In the test job above, it's used to create a test PostgreSQL database.

The deploy job will do the following:

  1. Check out the repository
  2. Install the dependencies
  3. Run the migrations against the production database
  4. Deploy to Heroku

Note: on: push will trigger the workflow for every commit pushed. The if: github.event_name == 'push' && github.ref == 'refs/heads/master' condition to ensures that the deploy job is only triggered for master.

Forking the repository and enabling the workflow

Begin by forking the GitHub repository so that you can configure GitHub actions.

Note: If you've already forked the repository, merge the changes from the master branch of the origin repository

Once forked, go to the actions tab on Github:

Enable the workflow by clicking on the enable button:

Now, when you push a commit to the repository, GitHub will run the workflow.

Heroku CLI login

Make sure you're logged in to Heroku with the CLI:

Creating a Heroku app

To deploy the backend application to Heroku, you need to create a Heroku app. Run the following command from the folder of the cloned repository:

Note: Use a unique name of your choice instead of YOUR_APP_NAME.

Checkpoint The Heroku CLI should log that the app has been successfully created:

Provisioning a PostgreSQL database on Heroku

Create the database with the following command:

Checkpoint: To verify the database was created you should see the following:

Note: Heroku will automatically set the DATABASE_URL environment variable for the application runtime. Prisma Client will use DATABASE_URL as it matches the environment variable configured in the Prisma schema.

Defining the build-time secrets in GitHub

For GitHub Actions to run the production database migration and deploy the backend to Heroku, you will create the four secrets referenced in the workflow in GitHub.

Note: There's a distinction to be made between build-time secrets and runtime secrets. Build time secrets will be defined in GitHub and used for the duration of the GitHub Actions run. On the other hand, runtime secrets will be defined in Heroku and used by the backend.

The secrets

  • HEROKU_APP_NAME: The name of the app you choose in the previous step.
  • HEROKU_EMAIL: The email you used when signing up to Heroku.
  • HEROKU_API_KEY: Heroku API key
  • DATABASE_URL: The production PostgreSQL URL on Heroku that is needed to run the production database migrations before deployment.

Getting the production DATABASE_URL

To get the DATABASE_URL, that has been set by Heroku when the database provisioned, use the following Heroku CLI command:

Checkpoint: You should see the URL in the output, e.g., postgres://username:password@ec2-12.eu-west-1.compute.amazonaws.com:5432/dbname

Getting the HEROKU_API_KEY

The Heroku API key can be retrieved from your Heroku account settings:

Heroku API key in the Heroku account settings

Creating the secrets in GitHub

To create the four secrets, go to the repository settings and open the Secrets tab:

GitHub repository secrets

Click on New secret, use the name field for the secret name, e.g., HEROKU_APP_NAME and set the value:

Checkpoint: After creating the four secrets, you should see the following:

GitHub repository secrets

Defining the environment variables on Heroku

The backend needs three secrets that will be passed to the application as environment variables at runtime:

  • SENDGRID_API_KEY: The SendGrid API key.
  • JWT_SECRET: The secret used to sign JWT tokens.
  • DATABASE_URL: The database connection URL that has been automatically set by Heroku.

Note: You can generate JWT_SECRET by running the following command in the terminal: node -e "console.log(require('crypto').randomBytes(256).toString('base64'));"

To set them with the Heroku CLI, use the following command:

Checkpoint: To verify the environment variables were set, you should see the following:

Triggering a workflow to run the tests and deploy

With the workflow configured, the app created on Heroku, and all the secrets set, you can now trigger the workflow to run the tests and deploy.

To trigger a build, create an empty commit and push it:

Once you have pushed a commit, go to the Actions tab of your GitHub repository and you should see the following:

Click on the first row in the table with the commit message:

Viewing the logs for the test job

To view the logs for the test job, click on test which should allow you to view the logs for each step. For example, in the screenshot below, you can view the results of the tests:

Verifying the deployment to Heroku

To verify that deploy job successfully deployed to Heroku, click on deploy on the left-hand side and unfold the Deploy to Heroku step. You should see in the end of the logs the following line:

To access the API from the browser, use the following Heroku CLI command, from the cloned repository folder:

This will open up the browser pointing to https://YOUR_APP_NAME.herokuapp.com/.

Checkpoint: You should see {"up":true} in the browser which is served by the status endpoint.

Viewing the backend logs

To view the backend's logs, use the following Heroku CLI command from the cloned repository folder:

Testing the login flow

To test login flow, you will need to make two calls to the REST API.

Begin by getting the URL of the API:

Make a POST call to the login endpoint with curl:

Check the email for the 8 digit token and then make the second

Checkpoint: The response should have the 200 successful status code and contain the Authorization header with the JWT token:

Summary

Your backend is now deployed and running. Well done!

You configured continuous integration and deployment by defining a GitHub Actions workflow, created a Heroku app, provisioned a PostgreSQL database, and deployed the backend to Heroku with GitHub Actions.

When you introduce new features by committing to the repository and pushing the changes, the tests and the TypeScript compiler will run automatically and if successful, the backend will be deployed.

You can view metrics such as memory usage, response time, and throughput by going into the Heroku dashboard. This is useful for getting insight into how the backend handles different volumes of traffic. For example, more load on the backend will likely produce slower response times.

By using TypeScript with Prisma Client you eliminate a class of type errors that would normally be detected at runtime and involve debugging.

You can find the full source code for the backend on GitHub.

While Prisma aims to make working with relational databases easy, it's useful to understand the underlying database and Heroku specific details.

If you have questions, feel free to reach out on Twitter.

Don’t miss the next post!

Sign up for the Prisma Newsletter