Ciabatta with Garlic & Basil

Birds at a bakery making a selection.
Image by Annie Ruygt

Previously, Vanilla with Candy Sprinkles covered how you could select your own “vanilla” JS demo and sprinkle in as many options as you like. But that blog post focused on Node.js, and now that Bun 1.0 has been released it is time for an update.

We could have updated the previous article in place with a series of “for Node.js do this, for Bun do that” choices, but that would have made for a much more cumbersome experience. So instead we went with a mostly parallel article with the choices already applied.

Overall the differences are:

  • Bun has built in file system, web server, and websocket features that enable you to create meaningful demos with zero require or import statements.
  • Bun has built in support for sqlite3 that can be used without installing any additional modules
  • Typescript is supported without any build step needed.
  • Bun supports ES import statements by default, and while it also supports cjs require statements, it discourages their use.

The Bun demos that you can select show off these features. You are encouraged try both the Bun and node.js demos side-by-side and compare the source code. If you are impatient, you can find selected test results online.

Additionally, Bun aims to be faster, and these demos do seem to load faster with Bun. But beyond that, these demos aren’t computationally intensive in a way that would show off performance improvements.

Let’s get started!

Baseline requirements

What we are looking for is a cross between Hello, World! and Rosetta Code, but for a full stack application. For our purposes, the baseline is a stateful web server. Ideally one that can be deployed around the globe, and can deliver real time updates. But for now we will start small and before you know it we will have grown into the full application.

A simple application that meets these requirements is one that shows a visitors counter. A counter that starts at one, and increments each time you refresh the page, return to the page, or even open the page in another tab, window, browser, or on another machine. It looks something like this:

welcome counter

As previously discussed, key to deployment is a package.json file that lists all of your dependencies, optional build instructions, and how to start your application. We are going to start very simple, with no dependencies and no build process, so the package.json file will start out looking like the following:

{
  "scripts": {
    "start": "bun server.js"
  }
}

Now to complete this we are going to need not only a server.js file, but also HTML, CSS, and image(s). As with some of the cooking shows you see on the television, we are going to skip ahead and pull a completed meal out of the oven. Run the following commands on a machine that has bun installed:

mkdir demo
cd demo
bunx @flydotio/bun-demo@latest

Once this command completes, you can launch the application with bun start. If you have authenticated and have flyctl installed, you can launch this application with fly launch followed by fly deploy. Should you decide to run fly launch, consider saying yes to deploying a postgres and redis database as we will be using them later.

You can play with this right now.

Don’t have Bun installed or a fly.io login? Deploy using Fly.io terminal or see our Hands-on) guide that will walk you through the steps.

Try Fly for free

If you are running it locally, open http://localhost:3000/ in your browser. If you have deployed it on fly.io, try fly open. If you are running in a fly.io terminal, there is a handy link you can use on the left hand pane.

Now take a look at server.js. It is all of 72 lines, including blank lines and comments. In subsequent sections we show how to make it smaller using available libraries, and how to add features. But before we proceed, lets save time and keystrokes by installing the node-demo package, which we will use repeatedly to generate variations on this application:

bun add @flydotio/bun-demo --dev

Using a real template

Inside the application you can see that the HTML response is produced by reading a template file and replacing a placeholder string with the current count:

contents = contents.replace('@@COUNT@@', count.toString())

While this is fine for this example, larger projects would be better served with a real template. bun-demo supports two such templating engines at the moment: ejs and mustache. Select your favorite, or switch back and forth:

bunx bun-demo --ejs

and

bunx bun-demo --mustache

In either case, this script will detect what changes need to be made, give you the option to show a diff of the changes, and to accept or reject the changes. This leads us to the second option: --force that will automatically apply the changes without prompting:

bunx bun-demo --ejs --force

Relaunch your application locally using bun start or redeploy it remotely using fly deploy.

A more substantial change

While Bun.server provides the means for you to create a capable HTTP server, express makes it easy to incrementally add routes, can be configured to automatically serve static files, and integration with template engines. To convert the demo to use express, run:

bunx bun-demo --express

Both ejs and mustache have integrations with express. Try switching between the two to see how they differ.

A real database

Maintaining a counter in a text file is good enough for a demo, but not suitable for production. Sqlite3 and PostgreSQL are better alternatives:

bunx bun-demo --sqlite3

and

bunx bun-demo --postgresql

Sqlite3 is great for development, and when used with litefs is great for deployment. PostgreSQL can be used in development, and currently is the best choice for production.

To run with PostgreSQL locally, you need to install and start the server and create a database. For MacOS:

brew install postgresql
brew services start postgresql
psql -U postgres -c "drop database if exists $USER;"
psql -U postgres -c "create database $USER;"
export DATABASE_URL=postgresql://$USER:$USER@localhost:5432/$USER

Be as weird as you want to be

The JavaScript ecosystem can be delightfully weird.

The next two options are frankly polarizing. People either love them or hate them. We won’t judge you.

First tailwindcss is a CSS builder that works based on parsing your class attributes in your HTML:

bunx bun-demo --tailwindcss

Next is typescript which adds type annotations:

bunx bun-demo --typescript

TypeScript should work with all of the options on this page, in many cases making use of development only @types. All of this should be handled automatically by node-demo.

Tailwind requires a build step, which can be run via bun run build. A change to the Dockerfile used to deploy is also required, which can be made using:

bunx dockerfile

@flydotio/dockerfile is actually a separate project with its own options for you to explore.

Object Relational Mappers (ORMs)

Adding databases was the first change that we’ve seen that actually makes the demo application noticeably larger, particularly with PostgreSQL once the code that handles reconnecting to the database after network failures is included. This can be handled by including still more libraries, this time Object Relational Managers (ORMs). Three popular ones:

bunx bun-demo --drizzle

and

bunx bun-demo --knex

and

bunx bun-demo --prisma

Knex runs just fine with vanilla JavaScript. Prisma can run with vanilla JavaScript, but works better with TypeScript. Drizzle requires TypeScript.

Prisma and Drizzle also require a build step.

A final note: if you switch back and forth between Sqlite3 and PostgreSQL, you may get into a state where the migrations generated are for the wrong database. Simply delete the prisma or src/db/migrations directory and rerun the bunx bun-demo command to regenerate the migrations.

Real Time Updates

If you open more than one browser window or tab, each will show a different number. This can be addressed by introducing websockets:

bunx bun-demo --websocket

The server side of web sockets will be different based on whether or not you are using express. For the first time we are providing a client side script which is responsible for establishing (and reestablishing) the connection, and updating the DOM when messages are received. This is a chore, and htmx is one of the many libraries that can be used to handle this chore:

bunx bun-demo --htmx

The next problem is that if you are running multiple servers, each will manage their own pool of WebSockets so that only clients in the same pool will be notified of updates. This can be addressed by using redis:

bunx bun-demo --redis

At this point, if you are using fly.io, postgres, and redis, you can go global:

fly scale count 8 --region ams,syd,nrt,dfw

Future explorations

While we have explored alternative implementation of the previous Node demo, this only scratches the surface. Bun includes much more:

node-demo is open source, and contains both the Node.js and Bun demos. issues, pull requests, and discussions are always welcome!

I hope you have found this blog post to be informative, and perhaps some of you will use this information to start your next application “vanilla” with your personal selection of toppings. Yummy!