Chi Core Knowledge
Deep Knowledge: Use mcp__documentation__fetch_docs with technology: chi for comprehensive documentation.
Basic Setup
package main
import ( "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" )
func main() { r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
})
http.ListenAndServe(":8080", r)
}
Routing
Basic Routes
r := chi.NewRouter()
r.Get("/users", listUsers) r.Get("/users/{id}", getUser) r.Post("/users", createUser) r.Put("/users/{id}", updateUser) r.Delete("/users/{id}", deleteUser)
// Method not allowed handler r.MethodNotAllowed(methodNotAllowedHandler)
// Not found handler r.NotFound(notFoundHandler)
Path Parameters
r.Get("/users/{userID}", func(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "userID") json.NewEncoder(w).Encode(map[string]string{"id": userID}) })
// Regex constraints r.Get("/articles/{date:\d{4}-\d{2}-\d{2}}", getArticleByDate)
// Catch-all r.Get("/files/*", serveFiles)
Route Groups
r := chi.NewRouter()
r.Route("/api", func(r chi.Router) { r.Route("/v1", func(r chi.Router) { r.Get("/users", listUsersV1) r.Post("/users", createUserV1) })
r.Route("/v2", func(r chi.Router) {
r.Get("/users", listUsersV2)
r.Post("/users", createUserV2)
})
})
Subrouters
r := chi.NewRouter()
// Mount subrouter apiRouter := chi.NewRouter() apiRouter.Get("/users", listUsers) apiRouter.Post("/users", createUser)
r.Mount("/api/v1", apiRouter)
Context
Request Context
func getUser(w http.ResponseWriter, r *http.Request) { // Path params userID := chi.URLParam(r, "userID")
// Query params
page := r.URL.Query().Get("page")
// Headers
auth := r.Header.Get("Authorization")
// Context values
user := r.Context().Value("user").(*User)
json.NewEncoder(w).Encode(user)
}
Context with Values
func UserContext(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "userID") user, err := findUser(userID) if err != nil { http.Error(w, "User not found", http.StatusNotFound) return }
ctx := context.WithValue(r.Context(), "user", user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
r.Route("/users/{userID}", func(r chi.Router) { r.Use(UserContext) r.Get("/", getUser) r.Put("/", updateUser) })
Middleware
Built-in Middleware
import "github.com/go-chi/chi/v5/middleware"
r := chi.NewRouter()
r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.Compress(5)) r.Use(middleware.Timeout(60 * time.Second)) r.Use(middleware.Throttle(100)) r.Use(middleware.Heartbeat("/ping"))
Custom Middleware
func TimingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) duration := time.Since(start) log.Printf("%s %s %v", r.Method, r.URL.Path, duration) }) }
r.Use(TimingMiddleware)
Authentication Middleware
func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization")
if auth == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if !strings.HasPrefix(auth, "Bearer ") {
http.Error(w, "Invalid token format", http.StatusUnauthorized)
return
}
token := auth[7:]
user, err := validateToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "user", user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
r.Route("/api", func(r chi.Router) { r.Use(AuthMiddleware) r.Get("/me", getMe) })
func getMe(w http.ResponseWriter, r *http.Request) { user := r.Context().Value("user").(*User) json.NewEncoder(w).Encode(user) }
CORS Middleware
import "github.com/go-chi/cors"
r.Use(cors.Handler(cors.Options{ AllowedOrigins: []string{"https://example.com"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, AllowCredentials: true, MaxAge: 300, }))
Response Helpers
render Package
import "github.com/go-chi/render"
type UserResponse struct {
ID string json:"id"
Name string json:"name"
Email string json:"email"
}
func (u *UserResponse) Render(w http.ResponseWriter, r *http.Request) error { return nil }
func getUser(w http.ResponseWriter, r *http.Request) { user := &UserResponse{ ID: "1", Name: "Alice", Email: "alice@example.com", } render.JSON(w, r, user) }
// Error response
type ErrResponse struct {
Err error json:"-"
HTTPStatusCode int json:"-"
StatusText string json:"status"
ErrorText string json:"error,omitempty"
}
func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error { render.Status(r, e.HTTPStatusCode) return nil }
func ErrNotFound(err error) render.Renderer { return &ErrResponse{ Err: err, HTTPStatusCode: http.StatusNotFound, StatusText: "Resource not found", ErrorText: err.Error(), } }
Manual JSON Response
func respond(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) }
func getUser(w http.ResponseWriter, r *http.Request) { user := User{ID: "1", Name: "Alice"} respond(w, http.StatusOK, user) }
Error Handling
type APIError struct {
StatusCode int json:"-"
Message string json:"message"
Details string json:"details,omitempty"
}
func (e *APIError) Error() string { return e.Message }
func errorHandler(w http.ResponseWriter, r *http.Request, err error) { var apiErr *APIError if errors.As(err, &apiErr) { respond(w, apiErr.StatusCode, apiErr) return }
respond(w, http.StatusInternalServerError, map[string]string{
"message": "Internal server error",
})
}
func getUser(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "userID") user, err := findUser(userID) if err != nil { errorHandler(w, r, &APIError{ StatusCode: http.StatusNotFound, Message: "User not found", }) return } respond(w, http.StatusOK, user) }
Patterns
Resource Pattern
type UsersResource struct { db *sql.DB }
func (rs UsersResource) Routes() chi.Router { r := chi.NewRouter()
r.Get("/", rs.List)
r.Post("/", rs.Create)
r.Route("/{id}", func(r chi.Router) {
r.Use(rs.UserCtx)
r.Get("/", rs.Get)
r.Put("/", rs.Update)
r.Delete("/", rs.Delete)
})
return r
}
func (rs UsersResource) UserCtx(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") user, err := rs.db.FindUser(id) if err != nil { http.Error(w, "User not found", http.StatusNotFound) return } ctx := context.WithValue(r.Context(), "user", user) next.ServeHTTP(w, r.WithContext(ctx)) }) }
// Mount resource r.Mount("/users", UsersResource{db: db}.Routes())
Production Readiness
Health Checks
r.Get("/health", func(w http.ResponseWriter, r *http.Request) { respond(w, http.StatusOK, map[string]string{"status": "healthy"}) })
r.Get("/ready", func(w http.ResponseWriter, r http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 2time.Second) defer cancel()
if err := db.PingContext(ctx); err != nil {
respond(w, http.StatusServiceUnavailable, map[string]string{
"status": "not ready",
"database": "disconnected",
})
return
}
respond(w, http.StatusOK, map[string]string{
"status": "ready",
"database": "connected",
})
})
Graceful Shutdown
func main() { r := chi.NewRouter() r.Get("/", handler)
srv := &http.Server{
Addr: ":8080",
Handler: r,
}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal(err)
}
}
Checklist
-
Recoverer middleware enabled
-
CORS configured
-
Request timeout set
-
Throttling enabled
-
Health/readiness endpoints
-
Graceful shutdown
-
Structured logging
-
Context-based values
When NOT to Use This Skill
-
Gin projects - Gin has more built-in features
-
Echo projects - Echo has different context patterns
-
Fiber projects - Fiber is Express-like and faster
-
Need built-in validation - Chi is minimal, use Gin or Echo
-
WebSocket-heavy apps - Other frameworks have better support
Anti-Patterns
Anti-Pattern Why It's Bad Solution
Not using chi.URLParam()
Manual param extraction Use chi.URLParam(r, "id")
Missing middleware order Wrong execution sequence Order middleware carefully
Not using context for values Global state issues Use context.WithValue()
Forgetting to call next.ServeHTTP()
Handler chain breaks Always call next.ServeHTTP(w, r)
No structured error handling Inconsistent responses Create helper functions
Using subrouters incorrectly Routes don't match Use .Route() or .Mount() properly
Quick Troubleshooting
Problem Diagnosis Fix
Route not matching Wrong path pattern Check regex constraints and params
Middleware not executing Order issue Place middleware before routes
Context value is nil Not set properly Use context.WithValue() in middleware
CORS errors Not configured Use cors.Handler() from go-chi/cors
Panic on missing param No default Check chi.URLParam() return value
405 Method Not Allowed Wrong HTTP method Verify route method matches request
Reference Documentation
-
Routing
-
Middleware
-
Patterns