Build an Express App Using Bun and Deploy It To Render With Docker
Tags: express, bunjs, docker, cloud
Published: 2023-10-25 15:30:49

In this post, we will scaffold a simple Express project using Bun, and then deploy it to Render using Docker. You’ll also get to see how much easier it is to get up and running locally with Bun than it is with Node. 

Let’s get started!

## Links

Some links for reference:

- Bun installation docs page [**here**](https://bun.sh/docs/installation)
- Bun + Express docs page [**here**](https://bun.sh/guides/ecosystem/express)
- Bun + Docker docs page [**here**](https://bun.sh/guides/ecosystem/docker)
- Bun hot reload docs page [**here**](https://bun.sh/guides/http/hot)

## Initial Setup

To get started, run this command to setup a Bun project in the folder of your choice:

```bash
bun init -y
```

Next, let’s make this a repo. Setup one on GitHub, then run the following commands:

```bash
git init
git branch -M main
git add .
git commit
git remote add origin https://github.com/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME.git
git push -u origin main
```

Now add `express` and its `@types` package:

```bash
bun add express @types/express
```

Update `index.ts` with this:

```tsx
import express from "express";

const app = express();
const port = 8080;

app.get("/", (req, res) => {
  // send a simple json response
  res.json({ message: "Hello World!" });
});

app.listen(port, () => {
  console.log(`Listening on port ${port}...`);
});
```

To test the local Express server, run this command:

```tsx
bun index.ts
```

Then if you visit http://localhost:8080/ in the browser, or send a `GET` request to that URL with something like Postman / Insomnia, you should see the JSON response:

```tsx
{
	"message": "Hello World!"
}
```

However, it would be nice to hot reload the server when we make changes, say to the response sent in `app.get()`.

Thankfully, Bun has built in hot reload support. We can run this command instead:

```bash
bun --hot run index.ts
```

So now if we change the `res.json()` to this:

```tsx
app.get("/", (req, res) => {
  // send a simple json response
  res.json({ message: "Hello from Bun & Express!" });
});
```

And if you then refresh the browser page or resent your GET request, you should see the updated JSON response:

```tsx
{
	"message": "Hello from Bun & Express!"
}
```

Let’s add this as a `script` to our `package.json`:

```tsx
"scripts": {
  "dev": "bun --hot run index.ts"
}
```

Now we can just run `bun run dev` instead. Noice!

## Comparison to Node

Now, let’s take a look at a simplified breakdown of what it would take to reproduce the same result using Node:

- Create a `package.json`: `npm init -y`
- Create a `tsconfig.json`: `npx tsc --init`
- Create `index.ts`: `touch index.ts`
- Install packages: `npm i express @types/express concurrently nodemon`
- Add the same boilerplate Express code to `index.ts`
- Watching file changes: `"dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\"”`

As you can see, here there are a few extra steps, and certain things that Bun does for us automatically, such as creating the `tsconfig.json` and `index.ts`, we have to do ourselves. We also have to install a few extra dependencies (`concurrently` and `nodemon`) to get the server up and running as well.

Please don’t take this as me bashing Node, that’s not my intention at all. All I’m trying to show is how much simpler things are with Bun, and I was honestly just so shocked at how fewer steps there were, and it’s provided a better developer experience in my opinion. I definitely think Bun is worth installing and trying out!

## Deploying to Render using Docker

### Setup Docker

At the root level of the project, run the following command to create the necessary Docker related files:

```bash
touch Dockerfile .dockerignore
```

Add the following code to `Dockerfile`:

```docker
# Dockerfile

# use the official Bun image
# see all versions at https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1 as base
WORKDIR /usr/src/app

# install dependencies into temp folder
# this will cache them and speed up future builds
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lockb /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile

# install with --production (exclude devDependencies)
RUN mkdir -p /temp/prod
COPY package.json bun.lockb /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production

# copy node_modules from temp folder
# then copy all (non-ignored) project files into the image
FROM install AS prerelease
COPY --from=install /temp/dev/node_modules node_modules
COPY . .

# [optional] tests & build
# ENV NODE_ENV=production
# RUN bun test
# RUN bun run build

# copy production dependencies and source code into final image
FROM base AS release
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /usr/src/app/index.ts .
COPY --from=prerelease /usr/src/app/package.json .

# run the app
USER bun
EXPOSE 3000/tcp
CMD ["bun", "run", "dev"]
```

Add the following code to `.dockerignore`:

```docker
# .dockerignore

node_modules
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode
Makefile
helm-charts
.env
.editorconfig
.idea
coverage*
```

You can test the `Dockerfile` locally if you have Docker installed by running the following command:

```bash
docker build --pull -t bun-express .
docker run -d -p 8080:8080 bun-express
```

If you should encounter the following error message during the `build` process:

```bash
error: lockfile had changes, but lockfile is frozen
```

This happens because it is likely that the `bun.lockb` file is not up to date. Simply run `bun install` to download and reinstall all the packages, and the issue should be resolved.

Make sure to push up any changes to your repo on GitHub at this point.

### Deploy to Render

Next, we can deploy the app to [**Render**](https://render.com/). There are of course tons of options to deploy an application in a Docker Container, I just found Render to be very beginner friendly and easy to use.

Create a free account, and once you’re logged in, on the dashboard page, click **New Web Service**.

Make sure **Build and deploy from a Git repository** is selected, and click **Next**.

Click the big purple **Connect Repository** button.

Select where you want to install Render, and then select if you want to install it for all your repos, or just a select few. I chose to install it just for this repo.

Click **Install**.

You should be back on the Render dashboard, and see the repo listed. Click the **Connect** button next to it.

Give it a name, I called mine `bun-express-render`.

Select the **Region** best suited to you.

Ensure the **Branch** is set to `main`.

The **Runtime** should automatically be set to **Docker**.

You can start with the Free tier.

Click **Create Web Service**.

The build will take a few minutes.

Once the build is finished, if you visit the URL, you should see the `message`!

![Hosted Express app on Render](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ba46zq0ht2k8gkf4q32h.png)

And that’s it! You’ve now created an Express app using TypeScript with Bun, and deployed it to Render using Docker!

Finally, [**here is a link**](https://github.com/andrews1022/demo-bun-express) to my repo with the full source code you can use as a reference in case you get stuck.

Cheers, and happy coding!