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 commands. I diligently copied each command, double- and triple-checking each character before pressing the Enter key. Soon I had committed the commands to memory and I no longer needed the checklist. Increasingly, 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 delivery and continuous deployment. Automating the deployment process removed the risk of mistyping commands and other errors associated with these repetitive tasks.

In this tutorial and its companion, you will learn how to use 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 that offers several advantages:

  • Secure by default, with no file, network, or environment access unless explicitly enabled
  • Supports TypeScript out of the box
  • Ships only a single executable file

Ready to start building and deploying Deno applications with speed and confidence? Let’s jump in.

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.

Our tutorials are platform-agnostic, but use CircleCI as an example. If you don’t have a CircleCI account, sign up for a free one here.

Creating a project directory

Create a new directory to hold all the project files:

mkdir deno_circle_ci_heroku

cd deno_circle_ci_heroku

Setting up a Deno server

In this section, you will use the Oak middleware framework to set up a Deno server. It is a framework for Deno’s HTTP server that is comparable to Koa and Express.

Create a file named server.ts and add this code to it:

import { Application, Router } from "https://deno.land/x/oak@v10.4.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. You will add the actual functionality for this endpoint once your pipeline has been set up successfully.

Notice that the port you are listening to is declared in an unusual way. 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

Review what you have done so far by running:

deno run --allow-net server.ts

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

API response

Writing tests for the application

Now you can write a test case for your API endpoint. Create a new file called server.test.ts and add this:

import { superoak } from "https://deno.land/x/superoak@4.7.0/mod.ts";
import { delay } from "https://deno.land/std@0.202.0/async/delay.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

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

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

The response should be similar to this:

Server is running on port 8000
running 2 tests from ./server.test.ts
it should return a JSON response with status code 200 ... ok (31ms)
exit the process forcefully after all the tests are done\n ...%

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 this command:

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

This command declares the web process for the app and the command to be executed on startup. Notice that we the command passes the port argument and binds it to the PORT environment variable provided by Heroku.

Next, create a new application on Heroku. You can do this from the Heroku dashboard. Click New, then click New App. Fill in the form. You can use any name and region you like.

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 to get is an API key. You will use this (along with the app name) 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 find it later.

Configuring CircleCI

Next, 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. In this folder, create a file named config.yml. In the newly created file, add this configuration:

version: 2.1

jobs:
  build-and-test:
    docker:
      - image: denoland/deno:1.37.1
    steps:
      - checkout
      - run: |
          deno test --allow-net server.test.ts
    # build job ommitted for brevity
  deploy:
    docker:
      - image: cimg/base:2023.09
    steps:
      - checkout
      - run:
          name: Deploy Main to Heroku
          command: |
            git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME.git main

workflows:
  build-deploy:
    jobs:
      - build-and-test
      - deploy:
          requires:
            - build-and-test # only run deploy-via-git job if the build job has completed

This 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. Also, the deploy job uses Git to deploy your latest code to your Heroku account.

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

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

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_circle_ci_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 fails because you have not provided your Heroku API key. To fix that, click the Project Settings button, and select 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, open your newly deployed app in your browser. The URL for your application should be in this format: https://<HEROKU_APP_NAME>-<RANDOM_NUMBER>.herokuapp.com/. You can find the generated domain name for your app on the settings page.

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.7.0/mod.ts";
import { delay } from "https://deno.land/std@0.202.0/async/delay.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, return 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, you have learned 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 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 can help your team create a more efficient software management process that automates the repetitive, mundane aspects of deployment so you 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.

Read more posts by Olususi Oluyemi