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.

Create a free account to save this page, take notes and track your progress. Sign in

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/http works with Chi, and code written for Chi works without it;
  • there's no custom context type to learn: you use r.Context() and chi.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

Groups, sub-routers and middleware with Chi
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/json and a validator.
  • Forgetting Recoverer. Without it, a panic in one handler is still caught by net/http per request, but you lose a clean 500 response and logging.
  • Mixing up Route, Group and Mount. Route creates a sub-router at a path prefix, Group shares middleware at the same path, Mount attaches an existing handler.
  • Middleware order. Use applies 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.

Was this page helpful?