Every Go service I’ve built over the years started the same way: pull in a router, wire up GORM, write the Redis connection helper again, copy the RabbitMQ consumer boilerplate from the last project, glue a JWT middleware together, and spend the first two days of a new project rebuilding the same plumbing I built last time.
Go’s standard library and ecosystem are excellent — that’s not the problem. The problem is that “lightweight and composable” means every team composes a slightly different stack, and the glue code between the pieces is where the bugs and the boredom live.
So I built GoFrame: a batteries-included web framework that bundles the libraries I already trust — Gorilla Mux, GORM, go-redis, Paho MQTT, Gorilla WebSocket — behind one consistent API, with a CLI that scaffolds projects and generates the boring parts. MIT licensed, Go 1.21+.
go get github.com/polymatx/goframe
Hello, World — with production defaults Link to heading
package main
import (
"net/http"
"github.com/polymatx/goframe/pkg/app"
"github.com/polymatx/goframe/pkg/middleware"
)
func main() {
a := app.New(&app.Config{
Name: "my-app",
Port: ":8080",
})
a.Use(middleware.Recovery())
a.Use(middleware.Logger())
a.Use(middleware.DefaultCORS())
a.Router().HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
ctx := app.NewContext(w, r)
ctx.JSON(200, map[string]string{"message": "Hello, World!"})
}).Methods("GET")
a.StartWithGracefulShutdown()
}
Two things I want you to notice. First, StartWithGracefulShutdown() — it listens for SIGINT/SIGTERM, stops accepting new connections, and drains in-flight requests before exiting. That’s not an add-on, it’s the default way to start. Second, the middleware you actually need in production — panic recovery with stack traces, request logging, CORS — ships in the box, along with gzip compression, per-IP rate limiting, and Prometheus metrics (middleware.Metrics() + middleware.MetricsHandler()).
Routing with groups and real middleware chains Link to heading
The router is Gorilla Mux underneath — battle-tested path matching, {id}-style params — with a group API on top:
api := a.Group("/api/v1")
api.GET("/users", getUsers)
api.POST("/users", createUser)
api.GET("/users/{id}", getUser)
api.PUT("/users/{id}", updateUser)
api.DELETE("/users/{id}", deleteUser)
Groups take middleware, so protecting a whole section of your API is one line:
jwtManager := auth.NewJWTManager("secret-key", 24*time.Hour)
protected := a.Group("/api/v1", auth.BearerAuth(jwtManager))
protected.GET("/profile", profileHandler)
Authentication that’s already written Link to heading
JWT, Basic Auth, and API-key auth are first-class packages, built on golang-jwt/jwt/v5:
// issue a token with role + custom claims
token, err := jwtManager.GenerateToken("user-123", "john", "admin",
map[string]interface{}{"department": "engineering"})
// validate / refresh
claims, err := jwtManager.ValidateToken(token)
newToken, err := jwtManager.RefreshToken(token)
// read claims inside a protected handler
func profileHandler(w http.ResponseWriter, r *http.Request) {
ctx := app.NewContext(w, r)
claims, _ := auth.GetClaims(r.Context())
ctx.JSON(200, claims)
}
One registration pattern for every backend service Link to heading
This is the design decision I care about most. Databases, Redis, MongoDB, RabbitMQ, MQTT, Elasticsearch — they all follow the same lifecycle: Register your named configs at startup, Initialize(ctx) once, Get("name") anywhere. Learn it once, use it for everything.
SQL (PostgreSQL / MySQL / SQLite via GORM):
database.Register(database.Config{
Name: "main",
Driver: database.PostgreSQL,
Host: "localhost",
Port: 5432,
User: "user",
Password: "pass",
Database: "mydb",
})
database.Initialize(ctx)
conn, _ := database.Get("main")
conn.DB().AutoMigrate(&User{})
conn.DB().Create(&user)
You get the full GORM API through conn.DB(), plus JSON-friendly null types (NullString, NullTime, GenericJSONField) for the real-world ugliness of nullable columns.
Redis (standalone or cluster):
cache.Register(cache.Config{
Name: "main",
Addrs: []string{"localhost:6379"},
Mode: cache.ModeStandalone,
})
cache.Initialize(ctx)
mgr, _ := cache.Get("main")
mgr.Set(ctx, "key", "value", time.Hour)
mgr.Incr(ctx, "counter")
mgr.SetNX(ctx, "lock", "1", 30*time.Second) // atomic set-if-not-exists
Strings, hashes, lists, sets, sorted sets, counters, TTLs — the operations you actually use, context-aware.
RabbitMQ and MQTT:
rabbit.RegisterRabbitMq("main", "localhost", 5672, "user", "pass", "/")
rabbit.Initialize(ctx)
conn, _ := rabbit.GetConnection("main")
conn.Publish(ctx, "queue_name", []byte(`{"data":"value"}`))
consumer := rabbit.NewConsumer("main", "queue_name", "consumer_tag")
for msg := range consumer.Channel() {
var data map[string]interface{}
msg.Decode(&data)
msg.Ack(false)
}
MQTT works the same way (mqtt.RegisterMqtt → Initialize → GetClient), which makes GoFrame comfortable in IoT setups where your API server also talks to a broker.
WebSockets with a hub, not a footgun Link to heading
Raw WebSocket handling is easy to get wrong — connection registries, write pumps, cleanup on disconnect. GoFrame ships the hub pattern ready-made:
hub := websocket.NewHub()
go hub.Run()
a.Router().HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("user")
if err := hub.Upgrade(w, r, userID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
hub.Broadcast([]byte("Hello, all!"))
The websocket-chat example in the repo is a complete working chat app on top of this.
Dependency injection without a framework religion Link to heading
There’s a small, thread-safe IoC container for wiring services — bind instances, factories, or singletons, and resolve or inject by struct tag:
c := a.Container()
c.Singleton("userService", func(c *container.Container) (interface{}, error) {
db, _ := c.Resolve("database")
return NewUserService(db.(*database.Connection)), nil
})
type Handler struct {
UserService *UserService `inject:"userService"`
}
c.Inject(&handler) // populates tagged fields
Singletons use double-checked locking, so concurrent resolves don’t race. If DI isn’t your style, ignore it — nothing else depends on it.
The CLI: scaffold, generate, serve Link to heading
The part that saves the most time day-to-day:
go install ./cmd/goframe
goframe new myapp # scaffold a complete project
goframe gen model User # GORM model
goframe gen handler user # HTTP handler
goframe gen crud Product # model + handler + routes in one shot
goframe gen middleware Auth # middleware skeleton
goframe serve # dev server with hot reload
goframe build # production binary
goframe migrate # run migrations
goframe new uses go:embed to carry the scaffolding templates inside the binary, so it works standalone — you don’t need the framework source tree checked out to spin up a new project.
For local development, make docker-up starts Postgres, Redis, MongoDB, and RabbitMQ in one command, and there are 11 runnable examples in the repo (basic, rest-api, database, mongodb, cache, websocket-chat, rabbitmq, mqtt, elasticsearch, ioc-container, and a full-stack app combining DB + cache + auth + metrics).
Is this “better” than gin, echo, or fiber? Link to heading
Wrong question — they’re different tools. Gin, Echo, and Fiber are lightweight routers with excellent raw throughput, and if you enjoy composing your own stack, they’re great. GoFrame trades a little of that minimalism for velocity:
- Router: proven Gorilla Mux instead of a custom one — slightly less raw speed, very well-understood behavior.
- Everything included: ORM, cache, NoSQL, messaging, WebSockets, auth, DI, metrics — no third-party shopping trip, no glue code.
- One lifecycle pattern across every infrastructure service.
- Code generation for the repetitive parts.
If your service is a bare-metal proxy where every microsecond counts, use fiber. If it’s a real application with a database, a cache, a queue, and auth — which is most of them — GoFrame gets you to the business logic on day one.
Engineering rigor, because “framework” is a trust word Link to heading
You’re trusting a framework with your production traffic, so the repo holds itself to CI standards: tests run with -race against Go 1.21, 1.22, and 1.23; golangci-lint enforces errcheck, staticcheck, gosec, and friends; a separate security-scan job runs Gosec; and CI compiles every example on every push so the docs can’t silently rot.
Try it Link to heading
go get github.com/polymatx/goframe
The code, docs, and all examples are at github.com/polymatx/goframe — MIT licensed. It’s young and I’d genuinely like feedback: if you hit a rough edge, want another driver supported, or think a default is wrong, open an issue. And if it saves you the two days of plumbing it was built to save, a star helps others find it.