---
title: "Vanilla with Candy Sprinkles"
description: "deploying Vanilla node.js apps on fly.io"
---

# Vanilla with Candy Sprinkles

![A bird behind an ice cream counter adding a second scoop of ice cream on top of a vanilla cone. On the bird's apron are the words 'Candy Sprinkles'](https://fly.io/javascript-journal/vanilla-candy-sprinkles/assets/vanilla-candy-sprinkles-cover.webp)

[Annie Ruygt](https://annieruygtillustration.com/)

Recapping where we are to date:

- There are [plenty of JavaScript frameworks to choose from](https://fly.io/blog/flydotio-heart-js/), and fly.io loves them all.
- Pretty much **all** of the big name frameworks are [delightfully weird](https://fly.io/blog/js-ecosystem-delightfully-wierd/).

Picking up where we left off, this blog post will describe literally dozens (and that’s actually an understatement as you will soon see) of considerably more, dare I say it, [vanilla](http://vanilla-js.com/) frameworks that you can assemble on your own and deploy to fly.io and elsewhere.

This can be overwhelming, so to make things easier we are going to define a baseline application that will be reimplemented to take advantage of various tools. The result will be:

- Educational. Seeing a bite sized working example is a great way to learn how a tool works.
- Useful starting point. Whereas large frameworks make a number of choices for you, being able to selectively include only the tools you need can provide you with a preconfigured configuration to build upon.
- Debugging aid. When a large system doesn’t behave the way you want it to, being able to reproduce and debug the problems on a smaller base not only can help you quickly narrow down the problem, and also can be used as a test case for a bug report.

Let’s get started!

## Baseline requirements

What we are looking for is a cross between [Hello, World!](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program) and [Rosetta Code](https://rosettacode.org/wiki/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](https://fly.io/javascript-journal/vanilla-candy-sprinkles/assets/welcome-counter.webp)

As [previously discussed](https://fly.io/blog/flydotio-heart-js/#package-json-enters-the-chat), 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": "node 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 node.js >= 16 installed:

```
mkdir demo
cd demo
npx --yes @flydotio/node-demo@latest
```

Once this command completes, you can launch the application with `npm run start`. If you have authenticated and have flyctl version 0.1.6 or later installed, you can launch this application with `fly launch` followed by `fly deploy`. When you 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 node installed or a fly.io login? Deploy using [Fly.io terminal](https://fly.io/terminal) or see our [Hands-on](https://fly.io/docs/hands-on/) guide that will walk you through the steps.

[Try Fly for free →](https://fly.io/docs/about/pricing/#free-allowances)

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:

```
npm install @flydotio/node-demo --save-dev
```

## Starting small

If you look at the top of the `server.js` file you will see a number of calls to `require()`. This is Nodes [CommonJS](https://nodejs.org/api/modules.html#modules-commonjs-modules) modules. Node also supports [EMCAScript](https://nodejs.org/api/esm.html#modules-ecmascript-modules) modules, which is what all the cool kids are using these days.

This requires opting in. You can let `node-demo` make the changes for you by running the following command:

```
npx node-demo --esm
```

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:

```
npx node-demo --esm --force
```

Relaunch your application locally using `npm run start` or redeploy it remotely using `fly deploy`.

## 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. `node-demo` supports two such templating engines at the moment: [ejs](https://ejs.co/) and [mustache](https://mustache.github.io/). Select your favorite, or switch back and forth:

```
npx node-demo --ejs
```

and

```
npx node-demo --mustache
```

Be sure to add `--esm` if you want to continue to use `import` statements.

## A more substantial change

While `node:http` provides the means for you to create a capable HTTP server, it requires you to be responsible for status codes, mime types, headers, and other protocol details. [express](https://expressjs.com/) will take care of all of this for you:

```
npx node-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:

```
npx node-demo --sqlite3
```

and

```
npx node-demo --postgresql
```

Sqlite3 is great for development, and when used with [litefs](https://fly.io/docs/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 next two options are frankly polarizing. People either love them or hate them. We won’t judge you.

First [tailwindcss](https://tailwindcss.com/) is a CSS builder that works based on parsing your class attributes in your HTML:

```
npx node-demo --tailwindcss
```

Next is [typescript](https://www.typescriptlang.org/) which adds type annotations:

```
npx node-demo --typescript
```

TypeScript should work with all of the options on this page, in many cases making use of development only [@types](https://www.npmjs.com/search?q=%40types). All of this should be handled automatically by node-demo.

Both of these require a build step, which can be run via `npm run build`. A change to the Dockerfile used to deploy is also required, which can be made using:

```
npx dockerfile
```

[dockerfile-node](https://github.com/fly-apps/dockerfile-node#overview) 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:

```
npx node-demo --drizzle
```

and

```
npx node-demo --knex
```

and

```
npx node-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 `npx 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:

```
npx node-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](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction) when messages are received. This is a chore, and [htmx](https://htmx.org/) is one of the many libraries that can be used to handle this chore:

```
npx node-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:

```
npx node-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
```

## Packaging alternatives

So far, we have been using `npm`, but `yarn` and `pnpm` are alternatives that may be better for some use cases:

```
npx node-demo --yarn
```

and

```
npx node-demo --pnpm
```

Each package manager organizes the `node_modules` directory a bit differently, so for best results when switching, remove the `node_modules` directory before switching:

```
rm -rf node_modules
```

Windows Powershell users will want to use the following command instead:

```
rm -r -fo node_modules
```

## Future explorations

While we have explored many options, this only scratches the surface. There are many alternatives to the libraries above, and many more things to explore. Examples:

- [React](https://react.dev/) can be run server side in a number of different ways, and can be run client side using a [CDN](https://en.wikipedia.org/wiki/Content_delivery_network) or self hosted scripts.
- In addition to React, there are a number of client side libraries like [Angular](https://angularjs.org/), [Lit](https://lit.dev/), [SolidJS](https://www.solidjs.com/), [Svelte](https://svelte.dev/), and [Vue](https://vuejs.org/). Coupling this with bundlers like [esbuild](https://esbuild.github.io/) and [rollup](https://rollupjs.org/), perhaps in a [monorepo](https://en.wikipedia.org/wiki/Monorepo) using [workspaces](https://docs.npmjs.com/cli/using-npm/workspaces) would make good starting points for larger projects.
- I welcome alternate implementations of this demo, perhaps using decidedly non-vanilla frameworks as a starting point. I’m particularly interested in implementations that support real time updates and globally distributed replications. If we get enough, perhaps we can maintain a catalog of pointers to these implementations.
- While this blog post has focused on local development and deployment on fly.io, there is no lock in here. Maintaining a catalog of pointers to blog posts that describe how to deploy this application elsewhere would be welcomed too. Again, bonus points for geographic distribution and real-time updates.

[node-demo](https://github.com/fly-apps/node-demo) is open source, so [issues](https://github.com/fly-apps/node-demo/issues), [pull requests](https://github.com/fly-apps/node-demo/pulls), and [discussions](https://github.com/fly-apps/node-demo/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!
