The first time I was tasked with maintaining a production server, I relied on a checklist my predecessor had left for me. The checklist contained all the maintenance steps along with their corresponding commands. In those early days, I religiously copied each command, double- and triple-checking each character before pressing the Enter key. Slowly but surely, the commands got committed to memory until one day I realized I did not need the checklist.

But then my tasks increased and I found that I could not type commands quickly enough. I tried shortening the process with custom scripts but it did not help much. Then the near-misses started and soon manual deployment was more scary than fun. Such is the reality of manual deployments; you are always one mistyped command away from disaster. Searching for a safer and more efficient alternative led me to continuous deployment. I quickly found that automating the deployment process removed the risk of mistyping commands and other errors associated with the human mind completing repetitive tasks.

In this tutorial and its companion, I will lead you through using CircleCI and Heroku to build a CI/CD pipeline for a Deno project. Deno is a simple, modern, and secure runtime for JavaScript and TypeScript. Deno offers these advantages to a project:

  • Deno is secure by default. There is no file, network, or environment access, unless you explicitly enable it.
  • Supports TypeScript out of the box.
  • Ships only a single executable file.

Prerequisites

Before you start, make sure these items are installed on your system:

  • An up-to-date installation of Deno.

For repository management and continuous integration/continuous deployment, you need:

  • A GitHub account. You can create one here.
  • A CircleCI account. You can create one here. To easily connect your GitHub projects, you can sign up with your GitHub account.
  • A Heroku account. You can create one here.

Creating a project directory

Create a new directory to hold all the project files:

mkdir deno_circleci_heroku

cd deno_circleci_heroku

Setting up a Deno server

In this section, we will use the Oak middleware framework to set up our Deno server. It is a framework for Deno’s HTTP server that is comparable to Koa and Express. To begin, create a file named server.ts and add this code to it:

import { Application, Router } from "https://deno.land/x/oak@v7.5.0/mod.ts";
import { parse } from "https://deno.land/std@0.99.0/flags/mod.ts";

const app = new Application();
const { args } = Deno;

const DEFAULT_PORT = 8000;
const port = parse(args).port ?? DEFAULT_PORT;

const router = new Router();

router.get("/", (context) => {
  context.response.type = "application/json";
  context.response.body = { data: "This API is under construction" };
});

app.addEventListener("error", (event) => {
  console.error(event.error);
});

app.use(router.routes());
app.use(router.allowedMethods());

app.listen({ port });
console.log(`Server is running on port ${port}`);

export default app;

This example shows a basic API that returns an “under construction” message when a request is sent to the index route. We will add the actual functionality for this endpoint once our pipeline has been set up successfully.

Notice that we declare the port we are listening to in an unusual manner. This is because when running the API, the port is provided by Heroku. You can read more about this here. The port will be provided in the deno run command, and will be parsed and used to run the application. If no port is specified (when the application is running locally), port 8000 will be used.

Running the application

We can run the application to see what we have done so far using this command:

deno run --allow-net server.ts

Navigate to http://localhost:8000/ to review the response.

API response

Writing tests for the application

It is time to write a test case for our API endpoint. Create a new file called server.test.ts and add this:

import { superoak } from "https://deno.land/x/superoak@4.2.0/mod.ts";
import { delay } from "https://deno.land/x/delay@v0.2.0/mod.ts";

import app from "./server.ts";

Deno.test("it should return a JSON response with status code 200", async () => {
  const request = await superoak(app);
  await request
    .get("/")
    .expect(200)
    .expect("Content-Type", /json/)
    .expect({ data: "This API is under construction" });
});

// Forcefully exit the Deno process once all tests are done.
Deno.test({
  name: "exit the process forcefully after all the tests are done\n",
  async fn() {
    await delay(1);
    Deno.exit(0);
  },
  sanitizeExit: false,
});

Running the test locally

Navigate to the terminal. From the root of the application, run the test using this command:

deno test --allow-net server.test.ts

You should see the following response below.

test it should return a JSON response with status code 200 ... ok (28ms)
test exit the process forcefully after all the tests are done
 ...%

With your test cases in place, you can set up your pipeline.

Configuring Heroku

In the root of the application folder, create a new file called Procfile. Please note that this file has no extension.

In Procfile, add the following command.

web: deno run --allow-net server.ts --port=${PORT}

This command declares our app’s web process and the command to be executed on startup. Notice that we are passing the port argument and binding it to the PORT environment variable provided by Heroku.

The next thing to do is create a new application on Heroku. You can do this from the Heroku dashboard. Click New and then click New App. Fill in the form. You can use the name and region you prefer.

Create Heroku App

Next, click Create app to complete the creation process. You will then be redirected to the Deploy view for your newly created application.

Your next task is to add a buildpack. To do this, click the Settings tab. In the buildpacks section, click Add buildpack.

Buildpack Button

This brings up a form where you can either select an officially supported buildpack or provide a URL for your buildpack. At the moment, Heroku does not have an officially supported buildpack for Deno, so you will need to provide the URL for a Deno buildpack instead. Paste https://github.com/chibat/heroku-buildpack-deno.git in the input field and click Save changes.

Add BuildPack URL

The last thing we need is an API key. In addition to the app name, you will use this to connect your CircleCi pipeline to Heroku. To get your API key, open the account settings page and scroll down to the API keys section.

Reveal API Key

Click the Reveal button and copy the API key. Save it somewhere you can easily access later.

Configuring CircleCI

Next, we need to add the pipeline configuration for CircleCI. For this project, the pipeline will consist of two steps:

  1. Build and test - In this step, you will build the project and run the tests for the application. If any of the tests fail, then an error message will be displayed and the process will be terminated.
  2. Deploy to Heroku - If the build and test step completed successfully, the latest changes are deployed to Heroku in this step.

At the root of your project, create a folder named .circleci and within it, create a file named config.yml. In the newly created file, add this configuration:

# Use the latest 2.1 version of CircleCI pipeline process engine.
version: 2.1

orbs:
  heroku: circleci/heroku@1.2

jobs:
  build-and-test:
    docker:
      - image: denoland/deno:1.10.3
    steps:
      - checkout
      - run: |
          deno test --allow-net server.test.ts

workflows:
  sample:
    jobs:
      - build-and-test
      - heroku/deploy-via-git:
          force: true # this parameter instructs the push to use a force flag when pushing to the heroku remote, see: https://devcenter.heroku.com/articles/git
          requires:
            - build-and-test

This configuration pulls in the Heroku orb circleci/heroku, which automatically gives us access to a powerful set of Heroku jobs and commands. One of those jobs is heroku/deploy-via-git, which deploys your application straight from your GitHub repo to your Heroku account.

The configuration specifies a job named build-and-test. This job checks out the latest code, creates a Docker image using Deno, and runs the tests in server.test.ts

Finally, the configuration specifies a workflow which runs the build-and-test job and then the heroku/deploy-via-git job. Notice that there is a requires option that tells CircleCI to run the deploy-via-git job only if the build job has completed.

Next, we need to set up a repository on GitHub and link the project to CircleCI. Review Pushing your project to GitHub for instructions.

Next, log into your CircleCI account. If you signed up with your GitHub account, all your repositories will be available on your project’s dashboard.

Click Set Up Project next to your deno_circleci_heroku project.

CircleCI detects the config.yml file for the project. Click Use Existing Config and then Start Building. Your first workflow will start running, but it will fail!

CircleCI Build Failed

The deployment process fails because we have not provided our Heroku API key. To fix that, click the Project Settings button, then click the Environment Variables menu option. Add two new variables.

  • For HEROKU_APP_NAME, add the app name you used in Heroku. The name will be either deno-heroku-circleci or a custom name if you created one.

  • For HEROKU_API_KEY enter the Heroku API key that you retrieved earlier from the account settings page.

Re-run your workflow from the start, and this time your workflow will run successfully. To confirm that your workflow was successful, you can open your newly deployed app in your browser. The URL for your application should be in this format https://<HEROKU_APP_NAME>.herokuapp.com/.

The Under Construction API response should be displayed.

Implementing the index route

Now that your workflow is up and running, you can implement the index route. For this API, it will return a list of hardcoded users. In the root of the application directory, create a new file called users.ts and add this code:

export default [
  {
    _id: "60d7b2ccfb294b7be115b6c4",
    isActive: true,
    picture: "http://placehold.it/32x32",
    age: 22,
    eyeColor: "green",
    name: "Harding Taylor",
    gender: "male",
    email: "hardingtaylor@lexicondo.com",
    phone: "+1 (839) 440-2917",
    address: "686 Harman Street, Reno, New Jersey, 5152",
    about:
      "Nisi irure aliquip aliquip Lorem. Elit ullamco commodo laborum aliqua commodo Lorem occaecat pariatur aute est reprehenderit ad. Qui nostrud enim aliqua consequat sit duis pariatur ex consectetur aute elit ad commodo officia.\r\n",
    registered: "2017-02-10T11:52:40 -01:00",
    friends: [
      {
        id: 0,
        name: "Donovan Nielsen",
      },
      {
        id: 1,
        name: "Barrera Hartman",
      },
      {
        id: 2,
        name: "Carmen Bean",
      },
    ],
    greeting: "Hello, Harding Taylor! You have 5 unread messages.",
    favoriteFruit: "banana",
  },
  {
    _id: "60d7b2cc411283f9ea9fdaff",
    isActive: false,
    picture: "http://placehold.it/32x32",
    age: 25,
    eyeColor: "blue",
    name: "Charlene Gibson",
    gender: "female",
    email: "charlenegibson@lexicondo.com",
    phone: "+1 (864) 446-3848",
    address: "132 Columbus Place, Clarksburg, Delaware, 281",
    about:
      "Exercitation voluptate cillum cillum do et voluptate officia Lorem sint. Ullamco quis ullamco et mollit. Veniam et labore aliquip elit fugiat proident labore.\r\n",
    registered: "2015-01-02T04:41:06 -01:00",
    friends: [
      {
        id: 0,
        name: "Morris Barrera",
      },
      {
        id: 1,
        name: "Aguilar Pearson",
      },
      {
        id: 2,
        name: "Denise Jacobs",
      },
    ],
    greeting: "Hello, Charlene Gibson! You have 5 unread messages.",
    favoriteFruit: "strawberry",
  },
  {
    _id: "60d7b2cc836c6ddb012508c1",
    isActive: false,
    picture: "http://placehold.it/32x32",
    age: 27,
    eyeColor: "blue",
    name: "Lavonne Barton",
    gender: "female",
    email: "lavonnebarton@lexicondo.com",
    phone: "+1 (958) 505-3633",
    address: "403 Ruby Street, Wintersburg, Pennsylvania, 1063",
    about:
      "Nisi sit cillum aute qui sunt ipsum deserunt ut. Fugiat laboris cupidatat mollit exercitation proident ad ut laboris nostrud amet amet dolor. Excepteur qui ea amet ut reprehenderit magna et proident sunt eu duis velit. Aliquip culpa proident aliqua dolore aliqua sint cillum anim amet duis esse nisi eu. Amet anim fugiat irure sit velit ad excepteur exercitation Lorem cillum sit deserunt dolore irure. Officia pariatur aliquip aliquip officia sunt sit dolore duis excepteur proident. Labore elit sunt ea deserunt officia nostrud.\r\n",
    registered: "2018-06-09T04:57:28 -01:00",
    friends: [
      {
        id: 0,
        name: "Barber Jimenez",
      },
      {
        id: 1,
        name: "Eliza Robbins",
      },
      {
        id: 2,
        name: "Charmaine Alexander",
      },
    ],
    greeting: "Hello, Lavonne Barton! You have 8 unread messages.",
    favoriteFruit: "banana",
  },
  {
    _id: "60d7b2cc112a8197462d9a8e",
    isActive: true,
    picture: "http://placehold.it/32x32",
    age: 32,
    eyeColor: "green",
    name: "Lea Evans",
    gender: "female",
    email: "leaevans@lexicondo.com",
    phone: "+1 (891) 524-3545",
    address: "725 Dennett Place, Alamo, New York, 1382",
    about:
      "Laborum dolor labore reprehenderit voluptate laborum ullamco non dolore magna officia ex velit. Ex nostrud duis ullamco cillum commodo occaecat pariatur nostrud nostrud occaecat incididunt minim eu. Minim ut ea quis laboris sunt.\r\n",
    registered: "2017-11-10T06:28:55 -01:00",
    friends: [
      {
        id: 0,
        name: "Fulton Oneal",
      },
      {
        id: 1,
        name: "House Watts",
      },
      {
        id: 2,
        name: "Isabel Melton",
      },
    ],
    greeting: "Hello, Lea Evans! You have 4 unread messages.",
    favoriteFruit: "strawberry",
  },
  {
    _id: "60d7b2cc3689b747a755bd89",
    isActive: true,
    picture: "http://placehold.it/32x32",
    age: 30,
    eyeColor: "green",
    name: "Jocelyn Harper",
    gender: "female",
    email: "jocelynharper@lexicondo.com",
    phone: "+1 (964) 464-2509",
    address: "524 Banner Avenue, Brenton, Texas, 1373",
    about:
      "Aliqua consectetur anim incididunt eu aute minim proident esse. Commodo eu tempor cillum veniam duis incididunt cupidatat. Tempor qui eu incididunt nostrud amet velit quis amet consequat. Deserunt nostrud eu laboris commodo irure fugiat dolore nisi in consequat ea in ullamco duis.\r\n",
    registered: "2015-04-10T11:13:10 -01:00",
    friends: [
      {
        id: 0,
        name: "Beatriz Carver",
      },
      {
        id: 1,
        name: "Gillespie Ferrell",
      },
      {
        id: 2,
        name: "Chris Boyer",
      },
    ],
    greeting: "Hello, Jocelyn Harper! You have 8 unread messages.",
    favoriteFruit: "banana",
  },
  {
    _id: "60d7b2cc03d9f88b8a833480",
    isActive: true,
    picture: "http://placehold.it/32x32",
    age: 39,
    eyeColor: "green",
    name: "Sharpe Wallace",
    gender: "male",
    email: "sharpewallace@lexicondo.com",
    phone: "+1 (979) 492-3250",
    address: "522 Madison Place, Charco, Missouri, 2144",
    about:
      "Voluptate culpa labore excepteur ut commodo veniam elit ea consectetur laboris adipisicing. Adipisicing ea officia qui reprehenderit. Ut ipsum elit irure id nisi. Enim cupidatat ea ea veniam est et enim nisi tempor. Minim laboris ipsum et ipsum mollit exercitation est labore voluptate cillum in dolor. Nostrud dolore labore et reprehenderit.\r\n",
    registered: "2017-11-12T10:09:50 -01:00",
    friends: [
      {
        id: 0,
        name: "Valenzuela Shelton",
      },
      {
        id: 1,
        name: "Margret Stuart",
      },
      {
        id: 2,
        name: "Rosie Nixon",
      },
    ],
    greeting: "Hello, Sharpe Wallace! You have 3 unread messages.",
    favoriteFruit: "apple",
  },
];

Next, update server.test.ts to match this code:

import { superoak } from "https://deno.land/x/superoak@4.2.0/mod.ts";
import { delay } from "https://deno.land/x/delay@v0.2.0/mod.ts";
import app from "./server.ts";

Deno.test(
  "it should return a JSON response containing users with status code 200",
  async () => {
    const request = await superoak(app);
    await request
      .get("/")
      .expect(200)
      .expect("Content-Type", /json/)
      .expect(/"users":/);
  }
);

// Forcefully exit the Deno process once all tests are done.
Deno.test({
  name: "exit the process forcefully after all the tests are done\n",
  async fn() {
    await delay(1);
    Deno.exit(0);
  },
  sanitizeExit: false,
});

In server.ts update the index route with this code:

router.get("/", (context) => {
  context.response.type = "application/json";
  context.response.body = { users };
});

Do not forget to import the users from users.ts:

import users from "./users.ts";

Commit your code and push to your Github repository.

git add .

git commit -m "Implement index route"

git push origin main

After the changes have been pushed to your Github repository, head back to your CircleCI dashboard, where the new workflow is running. When everything is completed, refresh your Heroku app. Your newly implemented route will return the list of users.

API response User List

Conclusion

In this tutorial, I have shown you how to set up a CI/CD pipeline for a Deno API using GitHub, CircleCI, and Heroku.

By automating the process of releasing new features, the risk of human error impacting the production environment negatively is greatly reduced. There is also a level of quality assurance added to the product because new features are deployed only if they pass the specified test cases.

These practices help create a more efficient software management process that automates the repetitive, mundane aspects of deployment so that your team can focus on problem solving.

The entire codebase for this tutorial is available on GitHub.

Thank you for reading!


Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest in solving day-to-day problems encountered by users, he ventured into programming and has since directed his problem solving skills at building software for both web and mobile. A full stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and blog posts on several blogs around the world. Being tech savvy, his hobbies include trying out new programming languages and frameworks.