DEV Community

Cover image for Build an Express App Using Bun and Deploy It To Render With Docker
andrew shearer
andrew shearer

Posted on • Updated on

Build an Express App Using Bun and Deploy It To Render With Docker

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
  • Bun + Express docs page here
  • Bun + Docker docs page here
  • Bun hot reload docs page here

Initial Setup

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

bun init -y
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

Now add express and its @types package:

bun add express @types/express
Enter fullscreen mode Exit fullscreen mode

Update index.ts with this:

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}...`);
});
Enter fullscreen mode Exit fullscreen mode

To test the local Express server, run this command:

bun index.ts
Enter fullscreen mode Exit fullscreen mode

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:

{
    "message": "Hello World!"
}
Enter fullscreen mode Exit fullscreen mode

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:

bun --hot run index.ts
Enter fullscreen mode Exit fullscreen mode

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

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

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

{
    "message": "Hello from Bun & Express!"
}
Enter fullscreen mode Exit fullscreen mode

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

"scripts": {
  "dev": "bun --hot run index.ts"
}
Enter fullscreen mode Exit fullscreen mode

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:

touch Dockerfile .dockerignore
Enter fullscreen mode Exit fullscreen mode

Add the following code to Dockerfile:

# 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"]
Enter fullscreen mode Exit fullscreen mode

Add the following code to .dockerignore:

# .dockerignore

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

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

docker build --pull -t bun-express .
docker run -d -p 8080:8080 bun-express
Enter fullscreen mode Exit fullscreen mode

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

error: lockfile had changes, but lockfile is frozen
Enter fullscreen mode Exit fullscreen mode

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. 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

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 to my repo with the full source code you can use as a reference in case you get stuck.

Cheers, and happy coding!

Top comments (0)