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.RegisterMqttInitializeGetClient), 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.