September 17, 2020

Modern Backend with TypeScript, PostgreSQL, and Prisma: Continuous Integration and Continuous 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.


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:

name: grading-app
on: push

jobs:
  test:
    runs-on: ubuntu-latest
    # Service containers to run with `container-job`
    services:
      # Label used to access the service container
      postgres:
        # Docker Hub image
        image: postgres
        # Provide the password for postgres
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          # Maps TCP port 5432 on service container to the host
          - 5432:5432
    env:
      DATABASE_URL: postgresql://postgres:postgres@localhost:5432/grading-app
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1
        with:
          node-version: '12.x'
      - run: npm ci
      # run the migration in the test database
      - run: npm run migrate:up
      - run: npm run test
  deploy:
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/master' # Only deploy master
    needs: test
    steps:
      - uses: actions/checkout@v2
      - run: npm ci
      - name: Run production migration
        run: npm run migrate:up
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
      - uses: akhileshns/heroku-deploy@v3.4.6
        with:
          heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
          heroku_app_name: ${{ secrets.HEROKU_APP_NAME }}
          heroku_email: ${{ secrets.HEROKU_EMAIL }}

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. Run the migrations against 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:

heroku login

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:

cd real-world-grading-app

heroku apps:create YOUR_APP_NAME

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:

Creating ⬢ YOUR_APP_NAME... done

Provisioning a PostgreSQL database on Heroku

Create the database with the following command:

heroku addons:create heroku-postgresql:hobby-dev

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

Creating heroku-postgresql:hobby-dev on ⬢ YOUR_APP_NAME... free
Database has been created and is available
 ! This database is empty. If upgrading, you can transfer
 ! data from another database with pg:copy
Created postgresql-closed-86440 as DATABASE_URL

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:

heroku config:get DATABASE_URL

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 settingsHeroku 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 secretsGitHub 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 secretsGitHub 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:

heroku config:set SENDGRID_API_KEY="REPLACE_WITH_API_KEY" JWT_SECRET="REPLACE_WITH_SECRET"

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

Setting SENDGRID_API_KEY, JWT_SECRET and restarting ⬢ YOUR_APP_NAME... done, v7

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:

git commit --allow-empty -m "Trigger build"
git push

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:

remote: https://***.herokuapp.com/ deployed to Heroku

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

heroku open

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:

heroku logs --tail -a YOUR_APP_NAME

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:

heroku apps:info

Make a POST call to the login endpoint with curl:

curl --header "Content-Type: application/json" --request POST --data '{"email":"your-email@prisma.io"}' https://YOUR_APP_NAME.herokuapp.com/login

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

curl -v --header "Content-Type: application/json" --request POST --data '{"email":"your-email@prisma.io", "emailToken": "99223388"}' https://YOUR_APP_NAME.herokuapp.com/authenticate

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

< Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjo4fQ.ea2lBPMJ6mrPkwEHCgeIFqqQfkQ2uMQ4hL-GCuwtBAE

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.

Join the discussion

Follow @prisma on Twitter

Don’t miss the next post!

Sign up for the Prisma newsletter