
Preface: The entire code I'm talking about is published here: https://github.com/trakora/production-go-api-template
Pull requests and issues are welcome.
The Starting Point
I came from the JavaScript world. When I needed an API, I typed:
npm install express
node index.js
Quickly mocked, quickly deployed. For every problem, there was a nice ready-made library that took care of everything. This worked well until I eventually had no clue what was actually happening in the background.
JS (and yes, TypeScript too) seduces you into quick solutions and sloppy code, at least in my case. Many of my projects don't have 10 developers and architects.
It was clear to me: I need a change of perspective.
I had the choice between Go and Rust. However, since Rust had a much higher learning curve, Go was the choice. Am I one hundred percent happy with it? No idea. But one thing I can say: It's much more fun than the old Node mess.
First Encounter with Go HTTP
When I started, there was practically nothing comfortable in the standard library. With Go version 1.22, I could really get started.
After "Hello, world" I stumbled upon terms like ServeMux
and HandlerFunc
.
In Node I had app.get("/posts/:id")
.
In Go I had to cobble this together:
mux := http.NewServeMux()
mux.HandleFunc("/posts/", postHandler)
All the articles I read used libraries like chi
or gorilla/mux
. But this time I wanted to know how far I could get with just the standard library and not permanently rely on external projects.
Three Articles That Helped Me
I found three articles that provided the foundation for getting started.
- Grafana – How I write HTTP services in Go after 13 years
- Fly.io Blog – Backend Basics
- Eli Bendersky – REST servers in Go – Part 1 (std lib)
All three essentially say: do everything with net/http
first, before reaching for a library.
My Minimal Prototype
package main
import (
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
No routing framework, no JSON helper. The thing runs, and I understand every line. Through the article Backend Basics I was also able to dive deep for the first time and understand how HandleFunc
works.
The standard library code is excellently written and immediately conveys what's happening.
From Prototype to Template Repo
More endpoints → more chaos.
As more endpoints were added, I needed structure: Config, Logging, Middleware.
This led to the Production-Go-API-Template. Folders like /cmd
, /api
, /pkg
and clean separation into Handler → Service → Repository.
Why I Use So Few Libraries
When I avoid external libraries, I gain three things – and noticeably so:
Control
I know every line running on my server. No magic, no hidden side effects. When a bug occurs, I can trace it instead of first searching through the issues tab of a GitHub project.
Fewer Upgrades
With npm install
I don't even understand the thousand upgrades I'd have to perform daily.
My build pipeline stays lean, and I don't have to check every few weeks whether version 3.4.2 of a framework brings another breaking change.
Better Learning
Those who commit to the standard library get Go explained practically in source code. Every handler, every interface is right there in black and white. This forces understanding – and ensures that I can later actually judge when an additional library is worthwhile and when it's not.
In short: Less plug-and-play, more insight. And that's exactly what makes the code more robust in the end and makes me a bit more confident as a developer.
Conclusion
Express was comfortable, but Go forces me to consciously decide: Which dependency is really necessary? The standard library easily covers 80 percent. For the rest, I choose libraries deliberately. My template repo grows organically without having to upgrade a framework every few weeks. I'm happy with the decision and will apply this doctrine to other projects in the future.