Migrate from Gin to GoFr
Summary
Gin handlers translate to GoFr cleanly. The biggest mental shift is the handler signature: func(c *gin.Context) becomes func(c *gofr.Context) (any, error) — you return data and an error instead of calling c.JSON(status, value). Middleware uses the standard net/http signature instead of Gin's gin.HandlerFunc.
Migrating with an AI assistant?
Hand https://gofr.dev/AGENTS.md to your coding assistant (Claude Code, Cursor, Codex, Aider). It contains the framework conventions, routing/binding/datasource patterns, and per-framework cheat-sheets so the assistant can translate handlers without you re-explaining GoFr.
Handler translation
Gin:
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
user, err := db.GetUser(id)
if err != nil {
c.JSON(404, gin.H{"error": err.Error()})
return
}
c.JSON(200, user)
})
GoFr:
app.GET("/users/{id}", func(c *gofr.Context) (any, error) {
id := c.PathParam("id")
user, err := db.GetUser(id)
if err != nil {
return nil, err
}
return user, nil
})
Request binding
Gin:
var input CreateUser
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
GoFr:
var input CreateUser
if err := c.Bind(&input); err != nil {
return nil, err
}
Query and path parameters
| Operation | Gin | GoFr |
|---|---|---|
| Path param | c.Param("id") | c.PathParam("id") |
| Query param | c.Query("q") | c.Param("q") |
| Default query | c.DefaultQuery("page", "1") | c.Param("page") (handle empty case) |
Middleware
Gin:
r.Use(func(c *gin.Context) {
start := time.Now()
c.Next()
log.Printf("%s took %s", c.Request.URL.Path, time.Since(start))
})
GoFr:
app.UseMiddleware(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s took %s", r.URL.Path, time.Since(start))
})
})
In practice you rarely need this in GoFr — request logging, tracing, and metrics are built in.
Libraries you can typically remove
After moving to GoFr, several Gin-side helpers usually become unnecessary because the framework already includes equivalents — keep whatever you'd still rather wire yourself:
otelginmiddleware → built-in tracing.gin-prometheus→ built-in metrics at/metrics.zap-ginrequest logging → built-in structured logging with trace IDs.- Manual
db.Ping()/ health endpoints → auto-exposed at/.well-known/health. - Custom retry / circuit-breaker code on outbound HTTP calls →
app.AddHTTPServicewith config.
Common gotchas
c.MustGethas no direct equivalent. Usec.Get(key)and handle the missing-value case explicitly.- Gin's middleware ordering matters at registration time. GoFr's default observability middleware runs before your custom
UseMiddlewarechain — assume tracing and metrics are already wired by the time your code runs. - Response wrapping is different. GoFr returns
{"data": ...}on success and{"error": ...}on error, and a plain struct returned from a handler is always wrapped. If your existing clients expect the raw object, return one of GoFr's special response types —response.Raw{Data: …}writes the payload directly with no envelope, andresponse.Responselets you control the shape. Returning an arbitrary struct does not bypass the envelope. - No
gin.H{}. Use plainmap[string]any{}or, better, named structs. - Validation isn't built in. Gin uses
binding:"required"tags via go-playground/validator by default. With GoFr, pick your validator explicitly.
Estimated effort
A typical 5-10 endpoint Gin service migrates in 1–2 engineering days. Most of the time goes to validating that observability output (traces, metrics) lands in your existing stack with the right names — not to handler translation.
Recommended order
- Move one endpoint to GoFr in a new file/service.
- Validate observability (traces and metrics) reach your existing collectors.
- Port remaining endpoints in batches grouped by data dependency.
- Drop now-redundant Gin middleware libraries.
- Decommission the old service when traffic has shifted.

