JAXLO.NET
Home Resume Book Reviews Sites Guestbook

Navigating the Go Web Development Ecosystem

Date: 2026-05-08

The Go web development ecosystem is fragmented and that is not necessarily a bad thing. The main contributor to this likely is how different website architectures can be. APIs, HTML templates, microservices are all major design tradeoffs that require different architectures. So depending on your design requirements, you can go in the direction that makes the most sense.

I ended up switching to Go entirely from Django. I started looking for a replacement because Python frameworks can be opinionated which I struggled to deal with while building Skillco. While some of these changes were possible in Django, they required digging into and overriding existing functionality in a way that felt hacky.

Going back to Flask/Quart was an option but it just felt dated.

The React and NPM ecosystem was the other obvious path. But the constant change scared me. New major framework versions every couple of years, dependencies that break each other, and a build pipeline that adds complexity before writing code. For a solo developer, that tradeoff did not make sense.

After that experience, I wanted to program with as little abstraction and magic as possible. Go fits that desire very well. Other reasons for the switch to Go included a lot of little quality of life improvements:

  • Go FMT - Enforces one coding style. (Helpful for LLMs and coworkers)
  • Error handling - Knowing where things will break and being able to plan for it. No more adding try/catch after finding prod errors
  • Static types and compile time verification prevents a whole class of errors
  • Package management and all the other little tools integrated with Go are amazing compared to the Python ecosystem
  • Go is faster than Python and uses less resources on my little VPS

I started experimenting with Go in 2023 and this website has been where I test each new approach.

Where to start

Getting started in Go web development is not as straightforward as other languages. Most ecosystems have a dominant framework that the community rallies around: Node has Express, Ruby has Rails, PHP has Laravel, Java has Spring Boot. Go does not have a leading framework.

Gorilla used to be the closest thing to a standard. Then in 2022 it was announced as unmaintained and a lot of projects scrambled to migrate. (It was later revived, but the scare stuck with people.)

The frameworks that exist today tend to solve different problems. Gin, Echo, and Fiber are fast HTTP routers aimed at APIs and microservices.

Buffalo tried to be Ruby on Rails for Go. Beego tried to be Django. Both seem to have slowed development drastically and neither really caught on.

The advice you hear most often is to just use the standard library. That sounds appealing until you actually try it. Until Go 1.22, the standard library did not even include a router with path parameters. You had to write one yourself or pull in a dependency just to handle /user/{id}. The standard library is a good foundation but it leaves a lot of gaps that you end up filling by hand on every project.

Databases

The database story in Go is similarly fragmented. The standard library gives you database/sql, which is a solid interface, but it is just an interface. You still need a driver and some way to map rows to structs.

GORM is the most popular option and it comes with all the advantages and disadvantages of a full ORM. It hides SQL behind method chains which is convenient right up until you need to do something the ORM did not anticipate. Then you are digging through documentation to find the escape hatch.

The approach I landed on is SQLC. You write plain SQL queries, run the code generator, and it produces type-safe Go functions. There is no runtime magic. The generated code is readable. Errors show up at compile time instead of in production. It fits the same philosophy that drew me to Go in the first place.

For smaller projects I have also just written queries directly against the driver with no abstraction layer. Tightly coupling the database to the handler is not architecturally clean but for a solo project it is fast and easy to follow. Both approaches work. It depends on how much the project is likely to grow.

What I use

My requirements for a stack are pretty specific:

  • Solo development
  • Server-rendered HTML monolith
  • Datastar for any reactive UI that needs it
  • Hand-written CSS because I enjoy it and CSS frameworks do not produce the output I want

Given those requirements, my preferred stack is SQLC for the database layer, the standard library router (or chi if a project needs middleware or route grouping), and Datastar when I need real reactivity without reaching for a JavaScript framework.

The piece that fills in the remaining gaps is Supermoto.

Supermoto is a small collection of Go functions I kept copying into every new project. It handles three things: opening a PostgreSQL connection pool, running forward-only SQL migrations, and parsing and serving Go HTML templates. That is it. No abstractions, no magic, no opinions about how you structure your routes or handlers.

The point is that each piece can be swapped out as the project grows. If I outgrow the standard library router I can drop in chi without touching the database or template code. If I want to replace the migration runner I can do that independently. Nothing is load-bearing in a way that creates a big migration later.

This is the file structure I use on most projects, including this site:

.
├── main.go - Entry point. Where the App struct is defined
├── go.mod
├── go.sum
├── sqlc.yaml - SQLC config
├── migrations/
│   ├── 001_create_users.sql
│   └── 002_add_sessions.sql
├── sqlc/
│   ├── generated/
│   │   ├── db.go
│   │   └── models.go
│   └── sql/
│       └── users.sql - Where I write the SQL SQLC uses
├── src/
│   ├── app.go - Main file with the application config and initialization
│   ├── handlers.go
│   ├── posts.go - A website specific file for blog post logic
│   └── router.go
├── staticfiles/
│   └── css/
└── views/ - Where the HTML files live

main.go wires everything together: it starts the database connection, runs migrations, registers routes, and starts the server. src/ contains the application logic. views/ holds the HTML templates. migrations/ are plain numbered SQL files. sqlc/ is where the queries live and where the generated code gets written.

It is not the only way to organize a Go web project but it has worked well for me across a few projects now. The structure is flat enough to navigate quickly and separated enough that things do not turn into spaghetti.