Battery included frameworks like Laravel, Rails and Next.js have a place in enterprise teams and startups, but the golang developer really has no direct competitor. I’ve landed on what I consider to be the building blocks for a perfect Golang web development stack. My choices optimize for delivery speed, simplicity and fun.
Here’s my battle-tested combo:
Sure, Go has built-in templating, but Templ takes it to another level. You get strong typing and a language server for great dev experience. Here’s a quick taste.
package contact
import (
"github.com/ebuckley/mtb/components/layout"
"github.com/ebuckley/mtb/lib/site"
)
templ Thanks(sp site.Site) {
@layout.Page(sp, ContactMeta(sp), nil, site.Contact) {
<h1 class="font-sans underline font-bold break-normal text-xl md:text-3xl">Thanks for getting in touch!</h1>
<p\>I'll get back to you as soon as I can.</p>
}
}
The beauty here is that a template looks a lot like a go code file. You can use strongly typed parameters, and get nice code completion for the fields that are passed in. Embedding sub templates is easy using as I am here where I reference my page layout template layout.Page
.
Templ is a code generator so you compile the .templ
files to plain old go functions. This means super fast performance in production and zero framework overhead.
Deploying a go web application just means putting a binary up on a server and keeping it running. This is all thanks to the standard library embed
package. Two lines is all it takes:
//go:embed public
var staticFiles embed.FS
The above takes my public
folder and embeds it all in the binary. You access the files using the standard file system interface. Code which works with the FS
or io
interfaces will ‘just work ™‘ with an embedded file same as a real file.
My http middleware setup can use a folder or an embedded FS as simple as this:
lib.FileServer(mux, "/", http.FS(staticFS))
This little middleware will take any path not served by an existing route and look up a public file from the embed.
The caveat is that you need to always reload to re-embed. I have a solution for you though. All you need is Air!
For golang developers, air is a secret weapon. It handles hot reloading so you can just hit save then refresh the browser. There is one gotcha; you need to configure around an infinite loop between air + templ.
To reload my app I use
cmd = "bunx tailwindcss -i styles/input.css -o cmd/server/public/output.css && templ generate && go build -o ./tmp/server ./cmd/server"
The compile command will compile tailwind css output and templ functions. This means that if you do not exclude certain folders and file patterns then an infinite loop of compilation and generation might happen.
To work around it use the following bit of config in your .air.toml
exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules", "cmd/server/public"]
exclude_regex = ["_test.go", ".*_templ.go"]
We ignore big folders like testdata
and node_modules
and places where the content changes as a result of the build I.E cmd/server/public
We also ignore _templ.go
files because they will be generated from our previous templ generate
step during compilation.
Look, there are a million ways to build web apps. But after trying pretty much everything out there, this stack just hits different. Simplicity and power that works perfect for me.
The best part about all this is that it starts small and can scale when you need it. No Over-engineering, no framework lock-in. Just standard library and small choices that can be replaced if needed.
If you’re starting with Go for web development in 2025, give this mix of libraries a shot. And hey, if you’ve got questions or want to share your own Go setup, @ me on the socials or contact form – I love talking shop.
Happy Coding!