The PET Stack - Part I

We are living in weird times right now. The Corona Virus is shutting down many parts of public life world-wide. Today is the first weekend of a two-weeks-long government-mandated stay-at-home period in the German state of Bavaria. Time to find something productive to do from home. I have worked on 8+ web apps in the past year. Today I want to share my favorite way of making them with you.

There are many tooling choices to make, when starting a new project. Which language or JavaScript flavor to use? How to do CSS? And how do we package our application? The JavaScript ecosystem offers lots of options, so many in-fact it can be quite overwhelming at times. I have tried out and have worked with many tools, but I couldn't be happier about the combination of tools, that I have found for myself right now, which I have started calling the PET Stack. It's ParcelJS + Elm + TailwindCSS. I will describe how to use each of them in more detail in this post and throughout the next couple of posts.

P is for Parcel

Bundlers package your app typically into an HTML, a JS and a CSS file at build time into something browsers can run efficiently. So most likely, if you're building apps for the browser, you're gonna need a bundler. You're gonna want to have a dev-server with hot-reloading to preview your progress and you're gonna want small and optimized bundles for production. Parcel can do all of that for you in a super simple way. It has out-of-the-box support for many different languages and packages HTML, CSS, JavaScript and luckily among others: Elm.

Here's a typical config file for Parcel:

# emptiness ... tumbleweed rolling along ...

Ok, I admit, the joke was not great, but it's true Parcel has great defaults and does not even need any configuration in most cases. Let's see how to set up a simple build with live reloading and transpiling JavaScript with babel.

Project setup

First, we'll set up a new project and install Parcel so it can be built. If you're following along, create a new folder and do the following in a terminal.

npm init -y
npm i -D parcel-bundler

Now, open up package.json in your editor of choice and add a couple of scripts.

  "scripts": {
    "start": "parcel src/index.html",
    "build": "parcel build src/index.html"

This way we're telling parcel to run a dev server on npm start. Running npm run build will produce a production build. Both commands reference a src/index.html file, which we will create in a second. Parcel will automatically take care of injecting necessary hot reloading runtimes for development and performing minification for production.

Next, let's add some actual content.

Adding a basic structure

We're gonna need three files for now and will put them into a src directory under the project root, like this:


Let's add index.html with a basic structure. It is used as Parcel's entrypoint in our npm commands. Note how we reference the other two files here.

<html lang="en">

  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Trying out Parcel</title>
  <!-- we are referencing our stylesheet here-->
  <link rel="stylesheet" href="./style.css">

  <!-- we will need this div later -->
  <div id="app"></div>
  <!-- we are referencing our js file here-->
  <script src="./index.js"></script>


Now we need a basic stylesheet. Nothing special yet, we just wanna get our first build working.

h1 {
  text-decoration: underline;
  background-color: lightgray;
  border-radius: 0.25rem;
  text-align: center;
  padding:  0.25rem;

Here's some dummy JavaScript, which simulates our web app until we add something fancier.

window.addEventListener('load', () => {
  document.querySelector('#app').innerHTML =
      Hello from JS!

Finally, run npm start in your terminal and you should see parcel building everything and firing up a dev-server on http://localhost:1234. Open your browser and make sure you're seeing this:

Screenshot of first parcel build That's what our web app looks like at the moment.

What just happened?

Wow, that was quick. How does Parcel know what to do? Parcel uses a set of great defaults and a little info it got from us, to give us what we need. But let's go through what happened step-by-step.

  1. We call Parcel through npm start, where we told Parcel to use src/index.html as the entrypoint.
  2. Parcel grabs index.html and finds a link to style.css. We are not doing any CSS processing at the moment but Parcel will just include the CSS as is.
  3. Parcel also finds a script tag which points to ./index.js and also includes it in the final bundle. If you peek around in the browser dev tools, you'll see that the JS file has been transpiled down to ES5. That's because Parcel uses babel by default and applies the preset-env to it. If you add a .babelrc you can configure Babel's behavior if you need more transforms than that.
  4. Parcel swaps out the src and href attributes in the final html output and replaces them with links to the final JS and CSS file. Parcel also injects hot module replacement runtimes and source maps into the output build. Try to change any one of the files we added, you should see the change immediately in the browser. Wohoo, hot-module-replacement for free 🥳.

We got a lot of what you would want from a bundler with zero-configuration from Parcel. Now, let's look at the production build. npm run build. This time we can find the production assets inside the dist/ folder. Open them up and you'll find that they all have been minified without us having to do anything for it. Parcel even takes care of adding a hash like src.3e2ab70.js to the files, which is great for setting long-term caching headers. (Side note: I found this post by Jake Archibald about http-caching best practices very good.)

Adding Elm to the mix

So far we built our HTML, JS and CSS. But I prefer to build apps with a functional language with strong type safety. If you have never heard about Elm and if I had to describe it, I would say, Elm is how I want programming to feel.

Enough praise for now. First let's do the following in a terminal.

npm i -D elm # install the elm compiler
npx elm init # create answer all the prompts with the default values

This will create an elm.json file, which works similar to a package.json file only for Elm. Next we'll create a src/Main.elm file with the following content.

module Main exposing (..)

import Html exposing (h1, text)

main =
    h1 [] [ text "Hello from Elm" ]

This is a tiny Elm program that simply renders a <h1> tag with some text to the screen. Last thing we need to do is initialize our Elm app from JavaScript. For this we open src/index.js and replace the contents with the following.

import { Elm } from './Main.elm';

Elm.Main.init({ node: document.querySelector('#app') });

Parcel will automatically compile our Elm code to JavaScript and import it. The second line with then initialize the Elm app into out app container. Now, run npm start again. Our Elm code is running correctly if you see this:

Screenshot of Elm parcel build Elm says Hello.


We saw that Parcel let's us build our app with basically zero-configuration. We checked out development and production builds. We added an Elm asset, which was also immediately built by Parcel and the resulting JS was bundled up into our build output. Compared to other tools like Webpack, we had the benefit of not having to write a config file or having to install and configure a bunch of plugins. So overall, we pieced everything together with Parcel pretty quickly and easily.

Some more noteworthy things about Parcel:

  • Parcel also has support for Webmanifest, Service-Worker, JSON, Web Assembly, environment variables and a ton more, as shown on the docs page.
  • Parcel v2 coming out. You can follow the progress on the road map.

Throughout the rest of this series, I'll continue to show how to build an application with Parcel, Elm and TailwindCSS. In Part II, we'll add some Elm code to make interesting things happen on the screen, while letting the compiler make sure we stay clear of bugs and bad practices.

More Posts