Go Web Frameworks: net/http, Chi, Gin, Echo and Fiber · The Standard Library and the Big Four
Chi: A Router That Stays Close to net/http
Chi adds route groups, sub-routers, URL parameters and a set of ready middleware while keeping the standard http.Handler signature, so every net/http middleware and handler works unchanged.
What you'll build
The books API with Chi: an /api/v1 group, a sub-router for /books, request IDs and panic recovery from Chi's middleware package, and a route that only authenticated requests reach.
The idea
Chi (github.com/go-chi/chi/v5) is a lightweight router, not a full framework. Its handlers are plain http.HandlerFuncs and its middleware are plain func(http.Handler) http.Handler, so:
- anything written for
net/httpworks with Chi, and code written for Chi works without it; - there's no custom context type to learn: you use
r.Context()andchi.URLParam(r, "id").
What it adds over the standard mux: route groups with their own middleware, sub-routers you can mount, and a collection of middleware (RequestID, RealIP, Logger, Recoverer, Timeout, Throttle…).
Choose Chi when you like the standard library's style and want better routing composition without lock-in.
The code
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func requireToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer secret" {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "sign in first"})
return
}
next.ServeHTTP(w, r)
})
}
func booksRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, []string{"Learning Go", "Concurrency in Go"})
})
r.Get("/{id}", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"id": chi.URLParam(r, "id")})
})
r.With(requireToken).Delete("/{id}", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
return r
}
func main() {
r := chi.NewRouter()
r.Use(middleware.Recoverer) // a panic becomes a 500 instead of killing the server
r.Route("/api/v1", func(api chi.Router) {
api.Mount("/books", booksRouter())
api.Get("/panic", func(w http.ResponseWriter, r *http.Request) { panic("boom") })
})
send := func(method, path, token string) {
req := httptest.NewRequest(method, path, nil)
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
body, _ := io.ReadAll(rec.Body)
fmt.Printf("%-6s %-20s -> %d %s\n", method, path, rec.Code, strings.TrimSpace(string(body)))
}
send("GET", "/api/v1/books", "")
send("GET", "/api/v1/books/42", "")
send("DELETE", "/api/v1/books/42", "")
send("DELETE", "/api/v1/books/42", "secret")
send("GET", "/api/v1/nope", "")
send("GET", "/api/v1/panic", "")
}
Output
GET /api/v1/books -> 200 ["Learning Go","Concurrency in Go"]
GET /api/v1/books/42 -> 200 {"id":"42"}
DELETE /api/v1/books/42 -> 401 {"error":"sign in first"}
DELETE /api/v1/books/42 -> 204
GET /api/v1/nope -> 404 404 page not found
GET /api/v1/panic -> 500 Run this on your computer — see “Get Started” for setup.
The program prints Chi's recovery message (with a stack trace) to standard error, which isn't shown here; the client just gets a clean 500.
Common mistakes
- Expecting a framework. Chi doesn't bind or validate request bodies; pair it with
encoding/jsonand a validator. - Forgetting
Recoverer. Without it, a panic in one handler is still caught bynet/httpper request, but you lose a clean 500 response and logging. - Mixing up
Route,GroupandMount.Routecreates a sub-router at a path prefix,Groupshares middleware at the same path,Mountattaches an existing handler. - Middleware order.
Useapplies in the order called; put recovery and request IDs first.
Key points
- Chi = net/http-compatible router: standard handlers and middleware.
- Route groups, sub-routers (
Route,Mount) and per-route middleware (With). chi.URLParam(r, "id")for path parameters.- A good default if you like the standard library and want no lock-in.
Further reading: chi documentation, chi on pkg.go.dev.
Practice questions
Why is Chi described as 'net/http compatible'?
Answer
Its handlers are standard http.HandlerFunc values and its middleware have the standard func(http.Handler) http.Handler signature, so any net/http middleware or handler works with Chi and vice versa. There is no custom context type.
What is the difference between Route, Group and Mount in Chi?
Answer
Route creates a sub-router under a path prefix; Group creates a set of routes at the same level that share extra middleware; Mount attaches an existing http.Handler (often another router) at a path.
Open in the app to mark lessons complete, take notes and track your progress.