25 Feb 2024
7 minute read
A while ago, I had this urge to track the family finances. I didn’t need or want anything complex; I just wanted to get a feel for how much we spend on food, bills, etc. In terms of features, I basically wanted something like Denaro. The only problem was that it’s a native Linux app, and I wanted a web app so that my fiancée and I could both track our finances. I looked around some more, and most of the web apps were paid and had a ton of features I don’t need, such as managing US taxes. Like any responsible developer, I decided to start a new side project and build my own personal finance manager.
Back then, I didn’t know that I would be writing this app twice: once with Next.js and once with htmx. Here is my experience of using both, which one I prefer, and why.
One of the reasons I decided to build my own app was to brush up on my frontend development skills. The last time I did any serious frontend development, Django and Bootstrap were still the way to go. I decided to try out what is considered the best by some in the JavaScript world: Next.js. I didn’t feel like writing my own components, so I decided to use shadcn/ui.
For the backend, I went with Supabase. This allowed me to only design my database schema and write the row-level security policies to get a fully functioning backend.
Here’s the revised version with corrected grammar and clarity improvements:
I have mixed feelings regarding the developer experience. The highlights for me
were Supabase Auth and Tailwind + shadcn/ui
. However, this is also where the joy ends.
The first problem is that Next.js is pushing the App router very hard, in fact,
it is the recommended way of doing things. But as soon as you want to submit a
form and invalidate the loaded data without refreshing the page, everything
starts to break down. With TanStack Query,
you get an amazing developer and user experience, but that requires the data
fetching to happen on the client side. This causes most of your files to use
use client
, which I feel somewhat defeats the purpose of the App Router.
At the time of writing, the errors that the App Router returns are not very informative. If you call a server component from the client component, you don’t get a clear error message telling you where you went wrong. Instead, you get some gibberish and end up doing bisect on the components tree to debug which component is causing issues.
Due to all of these problems, I ended up reverting to the Pages Router, which offers a much more pleasant and mature developer experience.
Another major issue with this stack is the Supabase client and RLS. The Supabase client doesn’t support some basic SQL features like transactions, and the RLS statements become quite ugly and unmanageable fairly quickly. If I were to do this again, I would write my own backend and connect to the database directly instead of using this method.
In conclusion, the developer experience can be really good if you use the right tools. However, in this case, the App Router and the Supabase client with RLS are not it.
The user experience during local development was very good. Unfortunately, when the app was deployed, I wasn’t that impressed. I used hosted Supabase and Vercel for my deployment. Due to the low number of users, cold starts occurred very often. In fact, every time I opened the app, I would encounter a cold start. This meant that everything loaded very slowly.
Additionally, I was running the app on a free Supabase instance, which goes to sleep if it doesn’t receive any activity for a week. There were many times when a week went by without me opening the app. To wake up the Supabase instance, I had to open the dashboard and click “resume,” which was very annoying.
Sure, all of these problems can be solved by self-hosting Supabase and the Next.js app. However, this means spending a lot of time managing a complex system with many moving parts, services, and configurations.
Htmx started gaining popularity, and after experimenting with it, I wanted to use it in a project. However, I wasn’t sure which project would be suitable. One day, while waking up my Supabase instance, I had a brilliant idea: I could start a new side project as a rewrite of an existing one and use HTMX in the rewrite!
This time, I made simplicity my top priority. With simplicity in mind, I came up with the following stack: Go + sqlite + templ + htmx + daisyUI. With the help of Go embed, the build process provides a single executable file, which can run anywhere. Simple!
If anyone wants to look at this project or use it for themselves, the code is available here.
This might be a subjective opinion because I am more comfortable with Go than TypeScript and prefer Go over TypeScript. But I do believe that for this type of web app, where all it’s doing is CRUD operations, this stack offers a better developer experience than Next.js and similar frameworks.
The experience of working with htmx is different from what most of us are used to. Many of us have forgotten the simpler times when the server would return HTML directly, and are now used to server returning data serialized to something like JSON, which the client then converts into HTML. Htmx brings back the old experience of the server returning HTML and the client just displaying it. Simple!
The fact that the server is responsible for rendering the HTML (almost) entirely eliminates the need for logic (and therefore JavaScript) on the frontend. You only need JavaScript on the frontend for very simple tasks, like opening a dialog. Simple!
This is also why I feel this stack offers a significantly better DX than modern JavaScript frameworks. As mentioned before, the experience of using no-code/low-code backends like Supabase RLS is not great. If I were to use a frontend framework like Next.js again, I would still prefer to have a separate backend. This means there would be some duplication of logic between the backend and frontend. I know that with App Router and server actions, this becomes a smaller problem, but then you are essentially writing a backend in JavaScript, which opens up a whole other can of worms, deserving of a separate blog post.
I might be tooting my own horn here, but honestly, this is one of the best web apps I’ve used recently. The load times are blazingly fast, and there are no spinners or loaders to be seen anywhere.
That being said, there is still room for improvement. For instance, the date
picker is the browser’s default one instead of a nice custom one. This also
holds true for the <select>
input. However, this can be solved relatively
easily: mount a custom React picker to just that one input. Alternatively,
you can be a responsible developer and write a custom one in vanilla JS.
Overall, I am very happy with the Go + htmx stack and would recommend it to most people for most side projects. I would even say that most production web apps could use htmx instead of JavaScript frameworks.
I know that in production, there are other factors to consider, such as downtime, scalability, and the ease of finding developers familiar with your stack. However, I also think that the industry has been somewhat brainwashed into believing that every app needs to be highly scalable and have almost zero downtime. The cold hard truth is, most web apps don’t have enough users to worry about these sorts of issues.
In any case, I know what I’ll be using for my next side project.