I love the web, but through my first few years of industry experience, I have begun to feel that some applications created are needlessly complex. Recently, I have seen too many instances of using Single Page Applications (SPAs) for projects that do not require that level of complexity.
Many of the apps I have built are, ultimately, a complex set of logic wrapped around both the front and back of a CRUD API. I watched a very interesting video by Theo.gg where he explained the appeal of HTMX and the role it serves in a web dev's toolbox. Theo also discussed and plotted a user's expectation of interactivity against the complexity needed for the frontend (and backend slightly). My biggest takeaway from the video was that the web was designed as a hypermedia driven architecture at its core and that engineers usually turn to React since it can handle the complexity of any app.
My go-to technology stack for professional projects recently has been React and a C# backend. The reason I favor this approach is that, anecdotally, most engineers have at least some understanding of React and can pick up what’s needed for the job. The C# backend stems from Visual Studio offering a very convenient template, and the company I work for is primarily a Microsoft/Azure shop, so it connects to nearly any Microsoft API with ease. My goal with this was to ensure that other developers could pick up the project easily and perform any potential future maintenance without hassle. (A lot of our projects are short-term, so there are often multiple handoffs between engineers.) I believe my goal was relatively successful with this approach; however, I have been pondering if this complexity is necessary for the projects I work on professionally. One of my biggest complaints about this setup is the mirroring of logic around the API endpoints. This is even more pronounced when TypeScript is involved, since the data types need to be explicitly defined in both the frontend and backend. This mirroring has led to duplicate tests and doubling of maintenance. Not to mention the amount of code that the application depends on just to get started. We have React, various NPM packages, Vite, .NET, NuGet packages, to name just a few. This means any bugs or vulnerabilities in those dependencies will become a bug or vulnerability in your app, leaving more work for some future engineer to figure out why our form buttons are mysteriously swapping values after a new build.
Theo's video led me to severely question whether the tech stack I often reach for—due to my comfort with it—may not be the best tool for the job. This led me down a wild rabbit hole of exploring the hypermedia-driven web. HTMX has been the guiding light in that journey, with the idea that if the HTML standard were simply expanded, we could achieve much more interactive web applications solely with HTML. Effectively, I interpreted this as meaning that by using HTMX, we would no longer need JavaScript! Now, I will admit this is 100% not true; JavaScript serves a specific purpose and is a very powerful tool for web applications, but, in my opinion, it should be used only when there is a specific need. Well... at least we can eliminate the need for a frontend framework along with the long first contenful paint and the complex auth token management that comes with it.
I was ecstatic when I came across the idea of using HTMX to eliminate the need for an SPA, React, or any frontend framework. This eliminates a whole potential attack vector for the application (no need to manage auth tokens in the frontend) and reduces the amount of code that must be maintained. (I sometimes get through a project and struggle to figure out what the previous developer's intent or reasoning was for their decisions in the codebase, so I have been striving to make my projects henceforth more maintainable and coherent—not to keep some poor engineer up all night.) I needed to know how it worked, so I began reading the Hypermedia book linked on the HTMX site, which is very educational about the original intent behind the design of the internet and why modern web development has evolved as it has. This led me on a new journey of exploring how to build a hypermedia-driven application with the goal of understanding how to construct interactive, stylish, and fast websites. I landed on using Go as the backend, the Go Templ package for HTML templates, HTMX to reduce the need for JavaScript, and TailwindCSS for styling (I have found this to be a very easy and reliable tool for generating stylish sites quickly). Sue me.
Why Go? Go is a high-level, typed language that compiles to a single binary. Period
When building an application, I need to focus on the user's needs rather than spending time managing memory or threads. Any modern-day app ought to be multithreaded (for the sake of concurrency), and Go makes this easy with its goroutines. A single binary also makes deployments 1000x easier, especially to a generic Ubuntu server in the cloud (or even a Raspberry Pi). Coming from extensive experience in both JavaScript and C#, a strongly typed language saves so many headaches in the development process and eliminates worries about potential type errors or forgetting to check a variable's type before executing. This turns a lot of debugging time into a simple five-second fix:
Well, ok, my build failed... hmmm
goes to line of original error
Oh, I just forgot to convert that new byte array to a string.
One of the other reasons I chose Go was its amazing standard library. Coming from the Node world, this is almost unbelievable. In every project I was involved in, there were at least 20 external dependencies needed to get the project off the ground. With Go, so many external dependencies can be avoided by using the standard library. Go still has great support for external packages, but it encourages a much more stable approach by forcing you to use them only when necessary.
I did end up using a few external packages to create this website, I will admit. I'm using a few Gorilla packages to simplify security and session management. I simply did not want to spend time reinventing the wheel for these features. The other main external package is Templ, a Go-powered templating engine that allows me to modularize my HTML into components easily.
The frontend uses a total of two external packages: HTMX and TailwindCSS. HTMX is the primary means to power the interactivity of the site without a large framework. I'm sorry, but I love Tailwind CSS—it's insanely easy to make a site look good with little effort. Swear to god, gun to my head, I will always advocate for TailwindCSS. Well maybe not if it's a real gun.
My main goal was to prioritize the developer experience and maintainability of the site. Go and HTMX accomplish this by reducing the amount of code that must be written and maintained. Creating a decent developer experience took some work, but I believe I achieved a satisfactory result.
The build chain requires three items to be created and run: the Tailwind CSS file, Templ templates compiled to Go code, and finally the Go binary to run the app. Tailwind and Templ have CLI tools with convenient watch commands to auto-generate output files when the input files change. There is a convenient tool called Air which provides hot reloading and watches for changes in Go code. Air watches for changes in Go code and the Tailwind output file. Tailwind watches all files for changes to class attributes, and Templ watches `.templ` files for changes. Now, by using the npm package concurrently, we can run all these commands in parallel, resulting in very fast refresh times.
The last part of the puzzle is updating the browser when a new Go binary is built. My original approach was to use the Air proxy server; however, I found that it was buggy and would seemingly decide at random when to include the hot reload JS. It also does not support web sockets. I read one of the GitHub issues in the repo, and the developer noted it would be a lot of work to support web sockets. I then learned that Templ includes a proxy server in its CLI tool, and it worked with my web sockets without any configuration. Finally, I configured the `npm run start` command to run the watchers and the proxy server in parallel. The only odd part worth mentioning is that a command needed to be added to Air to notify the Templ server on rebuild. The only slight annoyance with this system is that the Templ server refreshes both when the templ file changes and when Go code changes, leading to two browser refreshes when a templ file is modified.
Editor's Note: I actually fixed the double refresh somewhat using watch patterns on the Templ proxy server.
With this, I was able to create a simple site with a few pages and some minor interactive features, along with Tailwind CSS for easy styling as the cherry on top. I thought it might be helpful for other engineers to have a template of this setup for use in their own projects to get up and running quickly. Also, please make a PR if you have any suggestions or improvements to the setup—I would love some feedback.