Skip to main content

Migrate from chi to GoFr

Summary

chi is a router; GoFr is a framework. Migrating means dropping a lot of glue you wrote yourself — logging, tracing, metrics, datasource pooling, health endpoints, retry/circuit-breaker on outbound calls — and accepting GoFr's opinions on response shape and configuration. Handlers change from http.HandlerFunc to func(c *gofr.Context) (any, error).

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.

Mental model: router vs framework

chi's design goal is "a thin, idiomatic, net/http-compatible router". You bring everything else: a logger, OpenTelemetry instrumentation, Prometheus middleware, your own datasource pools, your own retry library, your own health endpoint. That's a feature when you want full control. It becomes a tax when every microservice in your fleet ends up reassembling the same five libraries.

GoFr makes the opposite trade: an opinionated handler signature in exchange for built-in observability, datasource clients, resilience on outbound HTTP, and health out of the box. If your chi service is mostly your own glue around the router, the migration mostly deletes code.

Handler translation

chi:

Go
r := chi.NewRouter()
r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    user, err := db.GetUser(id)
    if err != nil {
        http.Error(w, err.Error(), http.StatusNotFound)
        return
    }
    json.NewEncoder(w).Encode(user)
})

GoFr:

Go
app := gofr.New()
app.GET("/users/{id}", func(c *gofr.Context) (any, error) {
    id := c.PathParam("id")
    return db.GetUser(id)
})

The path syntax ({id}) is identical. You no longer touch http.ResponseWriter directly for typical JSON responses.

Request binding

chi has no body binding — you reach for json.NewDecoder(r.Body).Decode(&v). In GoFr:

Go
var input CreateUser
if err := c.Bind(&input); err != nil {
    return nil, err
}

c.Bind handles JSON, form, and multipart.

Param access

OperationchiGoFr
Path paramchi.URLParam(r, "id")c.PathParam("id")
Query paramr.URL.Query().Get("q")c.Param("q")
Headerr.Header.Get("X-Foo")Read in custom middleware (func(http.Handler) http.Handler) on the underlying *http.Request; c.Request is the abstract gofr.Request interface and does not expose Header
Raw *http.RequestrNot exposed on c.Request; c.Request is the gofr.Request interface (Param, PathParam, Bind, HostName, Params, Context). Reach the raw request through middleware if needed

Middleware

chi middleware is func(http.Handler) http.Handler — and so is GoFr's. Most chi middleware can be adapted by changing the registration call:

chi:

Go
r.Use(myMiddleware)

GoFr:

Go
app.UseMiddleware(myMiddleware)

You can usually delete chi middleware that exists only for cross-cutting infra (chi/middleware.Logger, chi/middleware.Recoverer, OTel/Prom adapters) — GoFr already provides those.

Route groups and sub-routers

chi's r.Route("/api/v1", func(r chi.Router) { ... }) pattern doesn't have a one-line equivalent in GoFr. The pragmatic translation is to register a path prefix per route, or wrap a small helper that closes over the prefix. For larger surfaces, model bounded contexts as separate handler structs and register their methods.

Render package

If you used go-chi/render for render.JSON(w, r, v), the GoFr equivalent is just return v, nil. Error responses are produced from return nil, err and shaped by GoFr's error handling.

Datasources

In a chi service you typically sql.Open yourself, manage a *sql.DB, set pool sizes, and instrument it. GoFr auto-initializes SQL and Redis from environment variables — set DB_DIALECT, DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME (or REDIS_HOST, REDIS_PORT) in configs/.env and gofr.New() wires the connection. Other clients are registered explicitly with a provider:

Go
app.AddMongo(mongo.New(mongo.Config{/* ... */}))

Inside handlers, use c.SQL, c.Redis, c.Mongo. SQL (MySQL/Postgres/Oracle/SQLite/SQL Server), Redis, Mongo, Cassandra, ScyllaDB, Couchbase, ArangoDB, Dgraph, SurrealDB. SQL/Mongo/Redis/Dgraph migrations are first-class — see migrations.

Observability

Where a chi service typically wires otelhttp, prometheus/promhttp, a logger, and a /healthz endpoint by hand, GoFr ships OpenTelemetry tracing, Prometheus metrics at /metrics, structured JSON logs with trace IDs, and /.well-known/health automatically.

Outbound HTTP

For service-to-service calls, instead of layering Hystrix-style libraries onto an http.Client:

Go
app.AddHTTPService("payments", "http://payments:8000")

Circuit breaker, retries, and rate limiting are configured per service.

Gradual adoption

Run a new GoFr service alongside your chi services. From GoFr, call into the chi service via app.AddHTTPService with built-in resilience. Move endpoints across at your gateway one bounded context at a time.

Frequently asked