Building for the Web without Node.js
I attend a handful of regional tournaments every year for the Pokemon TCG. One of my annoyances between rounds is manually refreshing a website to know when the next round starts. I hate the anxiety that comes with worrying about being late to my table and rushing to setup and start playing. I wanted a notification on my phone telling me where to go when pairings were posted. This seemed like a solvable problem, so I started coding.
I started simply, and made a front end using plain html, js, and css. I resisted the urge to grab the entire stack I’ve been using for years, and just stuck with the vanilla approach. What I ended up discovering was that modern web standards continue to get better, and I didn’t need all the libraries and frameworks I commonly choose to build web apps. I wasn’t worrying about gluing together the right sequence of plugins to get HMR and source maps, and compiling all the code back down to Javascript and CSS.
To be clear, I really like building things with Typescript and React on top of Vite, but the amount of tooling that you must adopt can lead to a lot of care and feeding long term. Typing npm outdated and npm audit can lead you down a rabbit hole of upgrades and breaking changes.
One of my goals with personal projects is always to make choices that won’t deeply upset future me, and eschewing node.js and npm seemed like a positive step in the right direction.1
Here’s where I ended up.
Go isn’t controversial. It’s one of the best ways to build servers that will reliably run and demand very little maintenance from you. Eventually I found the stdlib templating unreadable with any level of complexity, and migrated to Templ. Templ templates look very much like Go, and require far less mental overhead to comprehend and maintain.
The killer feature of Templ for me was named fragments within a template. It allowed rendering partial html payloads without having to write separate handlers and templates for each distinct partial. This allowed htmx integration nearly for free. The end result was a server rendered application that could still behave like a modern single page application.
Using esbuild meant I could create bundled and minified static assets without having to pull in an entire node.js toolchain with npm. Since ES6 Javascript modules and CSS imports are supported on modern browsers I could still write modular client side code that is easy to debug in development, and still realize the page performance gains in production. It was the right amount of tooling for what amounted to 500 lines of Javascript and 1000 lines of CSS.
I wouldn’t use this stack for everything. React’s component model handles complex UI state better with lots of side effects, and makes it possible to manage large client side applications. I had few interactive UI needs with this application though, and was able to handle most of those with a few htmx attributes.
When I come back to this project in six months, I won’t have a stack of deprecation warnings and package upgrades waiting for me. I won’t have to worry about the framework choices I made no longer being the correct way to build a client side application. I’ll still have a small amount of client side code that will just work. That predictability is what I was after, and so far it’s delivered.
Footnotes
-
The npm ecosystem had a rough 2025. In September, a self-replicating worm called Shai-Hulud compromised over 500 packages, serious enough for CISA to issue an alert. Earlier that month, chalk, debug, and 16 other packages were hijacked after a maintainer was phished—packages with over 2.6 billion weekly downloads combined. Go’s module system isn’t immune to supply chain attacks, but the attack surface is smaller with fewer transitive dependencies, and Go doesn’t allow arbitrary code execution during package installation like npm does with pre/post install scripts. ↩