How to Use Chi Middleware and Router Groups

Web
Use Chi middleware by passing it to `r.Use()` to apply it globally or to specific routes, and create router groups with `r.Group()` to organize routes and apply scoped middleware.

Use Chi middleware by passing it to r.Use() to apply it globally or to specific routes, and create router groups with r.Group() to organize routes and apply scoped middleware. This pattern keeps your handler logic clean while allowing you to stack authentication, logging, or recovery logic exactly where needed.

Here is a practical example showing global error recovery, a scoped authentication middleware, and a grouped API route:

package main

import (
	"net/http"
	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
)

// Global middleware applied to all routes
func main() {
	r := chi.NewRouter()

	// 1. Global Middleware (applies to everything)
	r.Use(middleware.RequestID)
	r.Use(middleware.RealIP)
	r.Use(middleware.Logger)
	r.Use(middleware.Recoverer)

	// Static files
	r.Get("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))

	// 2. Router Group with Scoped Middleware
	// This group applies "AuthMiddleware" only to routes defined inside it
	api := r.Group(func(r chi.Router) {
		r.Use(AuthMiddleware) // Only applies to /api/* routes
		r.Use(RateLimiter)    // Another scoped middleware

		r.Get("/users", GetUsers)
		r.Post("/users", CreateUser)
		
		// Nested group for admin routes
		admin := r.Group(func(r chi.Router) {
			r.Use(AdminMiddleware) // Only applies to /api/admin/*
			r.Get("/stats", GetStats)
		})
		admin.Route("/admin", func(r chi.Router) {
			r.Get("/stats", GetStats)
		})
	})
	api.Route("/api", func(r chi.Router) {
		r.Get("/users", GetUsers)
		r.Post("/users", CreateUser)
	})

	http.ListenAndServe(":3333", r)
}

// Example scoped middleware
func AuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Check for token
		if r.Header.Get("Authorization") == "" {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func GetUsers(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("User list"))
}

When defining groups, you can use r.Group() with a closure or r.Route() with a path prefix. The closure approach (r.Group(func(r chi.Router) { ... })) is preferred for applying middleware to a set of routes without a specific path prefix, while r.Route("/prefix", func(r chi.Router) { ... }) adds a path prefix and allows middleware scoping simultaneously.

Remember that middleware order matters. In the example above, AuthMiddleware runs before RateLimiter because it is registered first within the group. If you need to apply middleware to a specific route only, you can pass it directly to the route handler: r.Get("/public", middleware.NoCache, PublicHandler). This flexibility allows you to compose complex request flows without cluttering your handler functions.