# Deploying a Node.js app with CI/CD in Render

# Overview

I DEFINITELY underestimated how much work is involved in taking a project that runs on my local machine and making it accessible from the web.

In this article I will walk through the steps and tools involved in taking an app from your local machine and deploying it to Render. I will not, however, go into specifics of the technologies utilized or building the application.

[Link to GitHub Repo](https://github.com/bingliscodes/natours)

# Deploying to Render

## **Prerequisites:**

In order to deploy your app to render, you must first have a project in GitHub.

Deploying my app to Render was a relatively painless process.

1. Click the “Add new” button in the top right corner.
    
2. Click “Web Service”.
    
3. Sign into GitHub, then select the Repository containing the project.
    
4. Fill out the fields
    
    1. Note: make sure the “Start Command” field is correct. It is set to `$ node app.js` by default, but I had to change it to `$ node server.js` since I use that as my main entry point.
        
5. Select “Instance Type” (I used the “Free” one since it is for learning purposes).
    
6. Copy and paste in your environment variables from your `.env` file.
    
7. Click “Deploy Web Service” and let Render work it’s magic 🙂
    

Additionally, make sure that the Auto-Deploy setting is set to “After CI Checks Pass” from the default “On Commit” if you plan to integrate CI/CD (which I walk through how to do using GitHub Actions later on).

%[https://www.loom.com/share/65908700ccb44577b2170cf9dce724cc?sid=3035e839-9716-40c5-a42e-1b71ed11eb35] 

# Writing Unit Tests with Jest and SuperTest

[Fig](https://www.loom.com/share/65908700ccb44577b2170cf9dce724cc)uring out how to get Jest and SuperTest working was overwhelming due to the amount of information that exists about these, so I’ll try to explain the steps that I did to get it working as simply as possible, but first, what are Jest and SuperTest?

[**Jest**](https://jestjs.io/): A *framework* used for testing JavaScript code. Basically you define conditions that indicate whether or not a function or component is working as intended, and Jest will execute your tests so you can see what is or is not behaving as intended.

**SuperTest**: A Node.js library used for testing APIs by simulating requests and asserting responses.

## Preparing Your Environment

1. Install Jest and Supertest using npm
    
    `npm i jest --save-dev` and `npm i supertest --save-dev`
    
2. In your `package.json` file, add the following to your scripts: `“test”: “jest”`. Now you can execute all your testing via Jest simply by calling `npm test`
    
3. Inside your project root, create a folder `tests`. Since my application involves a REST API, the examples will be organized by controller. What’s important to note here is that each of these files will be treated as one Test Suite by Jest (Test Suites are the organizational unit that contain the unit tests).
    
4. Create the file to contain the tests with the convention `fileName.test.js`. For example, `tourController.test.js` in my application.
    
5. At the top of the file, require `request` from SuperTest, and `app` from the **entry point** of your application (recall that I said `server.js` was the entry point for my application)
    
    ```javascript
    const request = require('supertest');
    const app = require('../server');
    ```
    

## Writing Unit Tests

Recall that to thoroughly test our application, we will create unit tests using Jest, and then utilize functionality from the SuperTest library to handle the HTTP requests.

### What is Unit Testing?

[Unit testing involves testing the smallest possible functional unit of code](https://www.geeksforgeeks.org/software-testing/unit-testing-software-testing/). In the case of this demo app are the *route handlers*, which are essentially middleware that are executed when an HTTP request is made to the route specific by the handler.

### Your First Unit Test with Jest

While there is an abundance of information and [documentation for Jest](https://jestjs.io/docs/getting-started), I will try to distill down the most crucial information needed to get to get Jest *working* with your application, and then as you develop your testing suites refer to the documentation to troubleshoot specific issues.

Within your testing directory, create a file `sum.js` with the following code:

```javascript
// sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;
```

Next, create a `sum.test.js` file in the same directory:

```javascript
// sum.test.js
const sum = require('./sum');

// Demo test
test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});
```

Finally, from the root of the project, call `npm test` (the script we defined in “Preparing Your Environment”).

his will do is invoke Jest, which will locate your test files (the ones with the `.test` extension), and then it will execute all the tests specified within the Test Suite. In order to demonstrate what the different components of the function above do, I changed the `expect(sum(1, 2)).toBe(3)` to `expect(sum(1, 2)).toBe(4)` and put a screenshot of the results below.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1757970928753/a5580aa8-f998-43f9-a664-5aa55aed1f85.png align="center")

When the test fails, you can see the test description (what we input as the first argument into the `test` function) is displayed in red below the failed test, along with the actual `received` and `expected` values. This is the default behavior of Jest, but if you’d like to view the content in the `describe`, `it`, and `test` blocks *regardless of pass/fail status* then execute the command `npm test -- --verbose`

Side note on `it` vs. `test`: both of these functions essentially do the same thing in Jest, it’s primarily a [matter of readability](https://stackoverflow.com/questions/45778192/what-is-the-difference-between-it-and-test-in-jest). Moving forward I will use `it`, in which I describe the expected functionality of the test (i.e., what it *should* do).

**Understanding the Basic Components of a Jest test**

* **Test description:** This is the first argument entered into a the `test` or `it` function and should be used to describe the expected result of the test.
    
* **Expect:** Statements that are evaluated and determine whether a test passes or fails. Typically in the format of `expect(value).[operator](comparator)`.
    
    * In the example above, the value we are expecting is the *result* of calling `sum(1, 2)`, then we call the `toBe` operator and pass in the comparator value of 4, which checks if the result of `sum(1,2)` evaluates to 4.
        
    * This is a very simple example, so please refer to the [Jest documentation](https://jestjs.io/docs/expect#expectvalue) for more information
        

### Testing your API Endpoint with Jest + SuperTest

Now that we’ve reviewed the basic anatomy of a Jest test, let’s bring in the SuperTest `request` functionality.

Essentially what this function will do is asynchronously make an HTTP request to our application. Using async/await, we store the results in a variable `res`, then use chaining to specify the the request type and other details, such as what data we will send (in the event of a POST request).

```javascript
// userController.test.js
const request = require('supertest');
const app = require('../server'); // Main entry point to your app

describe('POST /api/v1/users/login', () => {
  const userData = {
    email: 'name@example.io',
    password: 'test1234',
  };

    it('should authenticate the user properly', async () => {
    const res = await request(app).post('/api/v1/users/login').send(userData);

    expect(res.status).toBe(200);
    expect(res.body.toHaveProperty('authToken'));
  });

 // Additional tests here
});
```

Three things I want to point out about the code snippet above:

1. `describe` is used here to [create a logical grouping of related tests](https://jestjs.io/docs/api#describename-fn). In the example above, I created a block that would run several tests on the specified endpoint.
    
2. We pass in `app` to the `request` function call. In order for this to work, your entry point module file must export the app (`module.exports = app` at the end of `server.js` in my case, where `app` is required at the top of `server.js`)
    
3. In order to get all my testing working, I had to wrap the code which initialized my server inside a condition that checks if we are in a testing environment:
    

```javascript
if (process.env.NODE_ENV !== 'test') {
  const server = app.listen(port, () => {
    console.log(`App running on port ${port}...`);
    console.log('mode is', process.env.NODE_ENV);
  });
```

## Integrating GitHub Actions

Now that we’ve deployed the app to Render and have some unit tests in place, it’s time to utilize GitHub Actions for our Continuous Integration and Continuous Deployment (CI/CD).

### What are GitHub Actions?

[GitHub Actions](https://docs.github.com/en/actions/get-started/understand-github-actions) is a service that enables us to automate the build, test, and deployment pipeline for our application. The basic building block of GitHub Actions is a [workflow](https://docs.github.com/en/actions/get-started/understand-github-actions#workflows), which will run one or more [jobs](https://docs.github.com/en/actions/get-started/understand-github-actions#jobs) when triggered by certain [events](https://docs.github.com/en/actions/get-started/understand-github-actions#events) in the repository (such as push or pull requests).

I’d recommend following the steps outlined in the [official documentation to get started with GitHub Actions](https://docs.github.com/en/actions/get-started/quickstart#creating-your-first-workflow). Once you have you have completed the steps outlined in the Quickstart guide, you can simply modify the YAML file to build and test your project by running `npm test`.

```yaml
jobs:
  run-unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v4
        with:
          node-version: '24' # Specify the node version of your project
      - run: npm ci
      - run: npm test
```

### Setting Environment Variables in GitHub Actions

This is the step that caused me the most trouble so I will outline how I resolved it. When building an app, there are almost always going to be details that cannot be exposed (and thus published) to a public repository such as GitHub. These include credentials for services, API secrets, or any other configuration details that shouldn’t be common knowledge. These details are typically saved in a `.env` file locally, and then injected into the code using a library such as Dotenv.

In order to set the environment variables for your workflow we will [use secrets in GitHub Actions](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets):

1. Navigate to your GitHub repository, then go to Settings &gt; Secrets and variables &gt; Actions &gt; Repository secrets.
    
2. For each variable in your `.env` file, add it as a secret by clicking “New repository secret,” then filling in the Name (as it appears in your `.env` file) and Secret fields.
    
3. In your GitHub Actions workflow, define your environment variables using the following format for each variable defined in your `.env` file.
    

```yaml
env:
  DATABASE: ${{secrets.DATABASE}}
  DATABASE_PASSWORD: ${{secrets.DATABASE_PASSWORD}}
```

By using the `${{secrets.VARIABLE_NAME}}` syntax we are accessing the `secrets` [context made available to us via GitHub Actions](https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#context-availability).

Once you have set all necessary environment variables, your app run as intended, using the unit tests we defined earlier as the condition for whether or not the code passes the CI test. Additionally, because of the change we made to the Auto-Deploy setting at the beginning, Render will automatically use this workflow to only re-deploy the application *when the continuous integration testing passes*. Congratulations! Your app is now live with CI/CD 😄

# Final Thoughts

I know that each and every one of these steps deserve it’s own whole article, but for the sake of brevity, learning, and sanity, I tried to include only the information that was absolutely essential. From my experience, one of the most daunting (but also exciting) parts about coding is opening up documentation and realizing just how much you **don’t** know.

As corny as it sounds, I consider myself a lifelong student. There will *always* be more to learn and better ways to do things. In fact, there are likely one (or more) better ways to accomplish exactly what I just did in this article. However, striving to make everything as “perfect” as possible from the start can impede progress. Let best practices guide you, and ask yourself often: “how can I improve this?”
