Making webapps using Golang Tailwind Templ Htmx Sqlc
Code here: https://github.com/domi-ninja/gotths-example
GoTTHS - Golang Tailwind Templ Htmx Sqlc
The whole fun of doing full-stack go is cooking your own stack. There are some like it, but this one is mine.
Inspired by
Reasoning for technolgies used
- Tailwind: I am fine with using plain CSS with variables, but prefer tailwind. As a bonus, this allows me to use things like https://daisyui.com/htmx-component-library/. It also makes it so that ai-generated styles are less likely to break existing styles in other places (not local to the component).
- Templ: Inspired by pocketbase, I have tried using a thin wrapper around
html/template
for server side rendering. It worked fine, but similar to tailwind, I have concluded that it is worth using. In this case, the slight annoyances are worth it to recieve some compile-time type safety for the templates, and for being able to use the database models generated by sqlc directly in my templates - HTMX is one of the many fun ways to make the server side rendered pages come to life. I am very familiar with it, but there is many other mini-frameworks that would do the same jobs just as well
- SQLC: SQLC is used for generating models and functions for interacting with a relational DB. It occupies a sweet spot between an ORM and having to write your own DB interfaces with a bunch of copy-pasting, and I feel very good about using this.
Files
$ tree -L 2
.
├── app.go entry point and routing
├── app.toml config file for our app
├── cmd
│ └── server
├── data.db sqlite3 db file for development
├── frontend
│ ├── assets frontend blobs
│ ├── components templ reused page components
│ ├── layouts templ layout components
│ ├── tailwind.config-v3.js configure tailwind here
│ ├── tailwind-input-v3.css change styles here
│ └── views templ actual views / partial views
├── go.mod
├── goose.sh bash wrapper for goose command
├── go.sum
├── handle_index.go handler for /
├── handle_post_postid_DELETE.go handler for delete existing post: DELETE to /post/{postId}
├── handle_posts_POST.go handler for creating new post: POST to /posts
├── handle_posts_postid_GET.go handler for rendering post: GET to /post/{postId}
├── handle_reload_WS.go websocket demo and `/reload` dev utility
├── handler_utils.go http handler utils for less code
├── internal
│ └── db_generated db binding code generated by sqlc
├── Makefile interact with this project using make
├── README.md
├── sql
│ ├── migrations numbered goose migrations
│ └── query named sqlc queries
├── sqlc.yaml
└── webhelp some minimal framework stuff that might be reused
├── config.go config parser
├── env_dev.go prod vs dev compile time differences example
├── env_prod.go ^
├── logger.go logger middleware that helps find where in the code things happen
├── render.go wrapping templ rendering in case we need that
├── utils.go
└── webhelp.go
Dev features
- All changes result in a recompile via
air
, which then triggers an homemade websocket-based page refresher in the browser (reload.js
) - Makefile
PRs welcome
This is a very small example, not a fully featured template. I have not researched if a good one exists.
This may never change since golang is very well suited to handknitted software like this. Still, one never knows
Missing features that would be cool to have
- [ ] Auth via a provider
- [ ] Auth, handrolled, but convering all the weird edge cases
- [ ] File upload using a configured S3 service
- [ ] A nice way to do form validation
- [ ] Common frontend components, e.g. Date and Calendar, using something like daisyui perhaps?
- [ ] Some sort of deployment story in addition to “golang can compile everything into a blob, figure it out yourself”
European Cloud Solution for sending emails with less headache
I wanted to make a nice, clean open source project. I want it to be small and simple to set up.
What open source projects usually do is have a config for SMTP_HOST, SMTP_PASSWORD etc and then just make it your (the users) problem to deal with this. I have set up my own email infra, I know how to do it, I have gotten it to decent reputation levels, etc, but I wanted something better for the users of my project.
This project also uses convex as the backend, so it is not an unopinionated clump of nodejs, it has certain characteristics that I like. I want the email sending thing to reflect this as well, it needs to be a scalable solution.
But I also felt iffy about sending all my users’ mails to some American company, just so they could log into their account on their own deployment of the open source project. Solutions I evaluated were: Use AWS for everything and then do infra as code on that and set up the email sending there, or pay 20$/mo to resend.com and have them handle the mails for you.
What I did instead is use a european offer called scaleway transactional email service. I did not know these guys existed. Their cloud was easy to sign up for, and their mailing service is just a REST API, and then they take care of delivery as best they can.
I did not even roll my own auth: I am using the auth setup that the convex people came up with.
IMHO that one exists in an interesting spot between hands-off (e.g. clerk) and roll your own. Betterauth is probably better, but I don’t know yet.
I took their Email thing for talking to the https://resend.com/ api and quickly adapted it for my own use. In case this is something you are interested in doing with convex specifically, here is the code: https://github.com/domi-ninja/websites.fail/blob/master/src/lib/auth/ScalewayEmailAuth.ts
Note how it gets used in convex auth setup in the usual way, check their docs for that, etc.
Anybody using my open source project could easily copy paste the Scaleway TEM
rest api call and do their own thing with a different provider, I am sure they exists!
Another alternative, even within convex, would be using node-mailer and talking SMTP to whatever service. I think that might be more brittle, I have seen some deeply unserious email sending services which will bounce/rate limit you etc. and then it becomes a hassle, so let’s not do that here.
TODO in this project (websites.fail):
- Get feedback about email sending success etc from scaleway and do something useful with that. Probably via webhook.
- Make it easy to spin up the convex backed on your own infra
Returning to the JS ecosystem (and its nice now?)
I felt a bit burned last time I seriously tried to get into modern full-stack typescript web frameworks. Part of this was also inexperience, but there is things that are objectively much better now:
Frameworks and tooling are better
… and more stable
i.e.
pnpm
exists so i can finally stop staring at a spinner for minutes every time i need to install packagesLLMs are good at writing React (specifically!)
Antropic claude is really good at fixing typescript linter issues
… and screwed up React hooks / prop drilling / etc.
… and npm package shenanigans
… and auto completing TS method signatures
Also I finally figured out how to do “select typescript version” in vscode so my LSP actually works (haha, oops)
A tiny project
So anyway I chugged a whole Pack of Kool-Aid and speedran making this super niche CRUD app by using Vercel, Clerk and Supabase.
The Vercel Postgres offer is weird, so I’m just cross cloud connecting to a supabase postgres url for now. This is not great, but this whole project is goofy anyway, so whatever.
React server components make sense to me. I did not care about performance considerations, but being able to put the S3 file uploading logic there to keep it simple was nice.
Should they have been named server side rendering components?
For a beginner, it is sometimes a bit confusing to have literally the same files be backend and frontend, but as a somebody who has written a few webapps in various ways, I understand the upsides of that, and the error messages are nice and clear.
Anyway, my app worked in a few hours and it was trivial to deploy including domain setup etc.
Clerk
Depending on the context, I am a third-party-service-hater on principle, and I have done my own auth a couple times, and it is not a huge deal. But it turns out that clerk is a really good offering, they made every single step for me as a dev nicer than using most consumer-facing apps. So credit where due!
NextJS build is slow
Configuring this helps: (thanks t3.gg)
next.config.js
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
This speeds it up to “only” 1 minute. It also results in some breakages not being clearly errored anymore. So sometimes, I had to comment these out, before then running pnpm build
locally to get the errors I needed, so that I could then fix stuff I screwed up in the app router. That was the only time I felt a bit lost.
Unrelated, there was a some typescript Linter errors that took minutes of compilation (every time) to finally surface, but the routes worked fine without fixing them. (I am guessing that to a typescript expert, this is normal..?)
Import fixing using LLMs is slow
I really missed Golang’s immediate enforced automatic reformatting and package importing on saving a file. Fixing imports “by hand” feels surprisingly horrible!
Maybe I can get Vscode to do this for me somehow..?
settings.json
"editor.codeActionsOnSave": {
"source.organizeImports": "always",
"source.addMissingImports": "always"
}
EDIT: Ok yes, that finally works (and not break my CTRL+S this time). Works really well in fact! Nice
Web devs on windows....
… consider developing on a server.
My reasoning
- Having a proper shell. I have tried really hard to make the windows replacements work, but WSL is too slow, the microsoft drop ins for scp etc. are too incomplete, and using cygwin is also messy.
- Making WIP commits every time i change machines and then squashing them later seems a bit tedious and unnecessary
How
Prereq
- Set up an ssh config for your host. (If you have never done this, check the vscode section to have the extension do it for you, simplest.). In this example, the hosts config name is
myhost
. - Don’t have the servers’ ports open to the internet. We will forward them.
- Set up git authentication on the server
Neovim Version
- Use
ssh -L 1234:127.0.0.1:1234 myhost
(ssh tunnel) in a terminal other than the one where your neovim is running - http://localhost:1234 should work now.
- Adjust 1234 to be the port of your app running on server obviously. There is also an ssh flag to make it block, open a tunnel, but not open a remote shell, and stuff like that. It’s surprisingly nice.
- Happy coding!
Vscode/Cursor version
- Search for
ssh
in the extension store, there is versions for vscodium that work too - Install that stuff
- Click somewhere on the left pane to connect to remote hosts
- You may have to set up an ssh config for that host, punch in the login user and hostname once, in the future you can get here much faster
- Make sure your extensions are installed and enabled on the remote host, this is a bit annoying sometimes.
- Vscode should automatically forward ports that are opened on the server while it is running
- Happy coding!
"Project soup" is my Golang Rite of Passage
I have been a software dev for 10+ years, but learned Golang recently.
A big moment for me was when I started remixing parts of multiple open source projects into my own little project soup.
By the way, I’m being ethical here: Projects were MIT licensed, I am not selling anything I made this way, etc.
Anyway, the Project soup:
- Built from the ground up
- Know exactly how it works
- “Stolen” code integrated as modules
- Project setup did not become a mess
- Still compiles super fast
I think this is an example where many of the strengths of Golang come together in a beautiful way, and will try to shine some light on what made it possible for me to do this.
1 Go developers actually KISS
(Keep It Super Simple)
In Golang, there many things the language designers intentionally saved you from:
- Doing Object Oriented Programming with
class
es - Deciding where to handle
Exception
s - Extreme use of the type system
- Doing too much on one line of code
- Fiddling with code formatting
But!
There is also things that would be allowed, but Golang developers seem to avoid:
- Lots of abstractions piled on top of each other
- Extremely function flavored programming
- Bad use of libraries
- Overusing generics
The reasons for why Golang devs do not tend to do these things are probably complex.
I believe Golang attracts pragmatic developers that align with the goals and vibes of the language and prioritizing “getting shit done” over many other things, as well as KISSing.
Golang also changes the developers mindset to some degree by the errors and warnings the compiler emits. It does a good job of severly discouraging many practices without making them completely impossible. I would argue this is relevant, it certainly worked for me, and is a cool use of nudging. If ever there was a population that needed to be nudged, it is software developers.
Anyway, the absence of these practices is wonderful, and it is the most important piece for making the project soup possible, because reading other peoples code is not a cognitive nightmare! Hooray.
2 Nice Standard Library, Bro
I think the golang stdlib strikes a difficult balance very well. It embodies the KISS culture I was talking about while offering most of the things a golang user needs to do most of the time, and doing them well enough.
While I acknowledge various criticisms of it, developers did not feel the need to use a bunch of competing alternative standard libraries all over their code base (like in JS).
Notably neither the language itself, nor the golang stdlib has also not been fucked up by big changes to the language. I do not care how big the flaws in your language or stdlib are: If you make breaking changes, it is almost always a horrible trap. Just leave it alone.
Golang has been super careful to avoid this horrible trap. Thank you.
3 The Modules / Library system does not suck
The way imports/modules work is clear, debugging them is intuitive, and nothing major about them is messed up. If this sounds like table stakes to you, well, I wish you the best of luck with other languages.
Tips
I used some of the
replace github.com/foo/bar => ./bar
syntax in mygo.mod
files so that I could develop two separate projects in tandem. This is awesome.Take note of the
go vendor
command, which automates the annoying work of cloning package manager dependencies into your own source code.
The end!
I could ramble about cool Golang things more, but writing this is taking too much time.
PS
If you are trying out Golang, and annoyed about “unused variables” being a compiler error: It makes sense, don’t let it stop you from trying out this language :)
Poop Status Magazine
I think we need some sort of website that tracks the current enshittification status of popular companies.
Consider this example:
Status | Example Product | Reasoning | Recommendation |
---|---|---|---|
Growth | Cursor.sh | Everyone agrees product is pretty great. Newly released features actually add value. Occasional big bugs including security issues, but they get fixed quickly. | Enjoy the product while you can :) |
Bloat | Discord | Product is solid, but company is clearly flailing for a way to monetize. Bad decisions on the horizon. | Deprioritize the product when looking for solutions |
Enshittified | Dropbox | Used to be good, now is focusing on nagging end users constantly | Consider literally any rival products, there will be better ones! |
Stratified Shit | Microsoft Excel | Dinosaur offering shitty experience, mostly successful due to network effect | Make using the product somebody else’s problem |
Expanding on: Somebody else’s problem
Let’s take our example of Microsoft Excel.
- If you run your own small business, you can escape it with no problems. Use whatever you like instead.
- If you need to edit MS Excel sheets that people send you, you could try using onlyoffice. Do not tell them about it and see if anything major breaks. Play a bit dumb if needed.
- If you are in a big company and forced to use it, deal with it proactively. Find all the tricks to create a smooth running operation for yourself in the fossilized layers of “poo”. There is no shame in this!
Unshittifiable products
Nextcloud has its flaws, but it syncs my stuff just fine. For almost ten years. Similar to Dropbox, they released a flurry of enterprise features to help sell it to governments agencies and businesses.
But you know what? They did not start nagging their end users. Their incentives are not aligned in this way, because they are not an incredibly overvalued SV company that suddenly feels the need to squeeze their 300M users for all they are worth “because oops I guess we need start making a lot of money right now”.
By the way, I would like to praise any government agency buying services from Nextcloud, because they are following the crucial rule of not buying closed source products as a government agency. Such products will inevitably turn into an enterprise-grade leech that sucks up too much tax money (due to game theory reasons).
And yes, open source to the rescue: If Nextcloud tried to do that, somebody would fork the project and release a guide on how to deploy NextNextCloud, and the bosses of the affected users could hire some consultants to replace their Nextclouds, and everything would be completely fine.
Summary
Microsoft bad, Open Source good