Understanding Middleware in Go with Chi

Table of contents
Middleware is a powerful concept in web development, allowing you to execute code before or after a request is processed by your application. In Go, middleware is commonly used to handle tasks like authentication, logging, request validation, and more. The Chi router, a lightweight and idiomatic HTTP router for Go, provides an elegant way to implement middleware. In this post, we'll explore how middleware works in Go with Chi, complete with examples.
What is Middleware?
Middleware in Go acts as a bridge between the HTTP request and response. It’s essentially a function that intercepts the request, performs some logic, and either passes control to the next handler or terminates the request. Middleware is often used for:
Authentication/Authorization: Verify user credentials or tokens.
Logging: Record request details for debugging or monitoring.
Request Modification: Add headers, validate input, or modify the response.
Error Handling: Catch and handle errors gracefully.
In Chi, middleware is applied to routes or groups of routes and executed in the order they are defined.
How Middleware Works in Chi
Chi is a lightweight, idiomatic router built on top of Go’s net/http package. It supports middleware through a chainable, composable approach. Middleware in Chi is implemented as an http.Handler or a function that wraps an http.Handler, allowing you to manipulate the request or response before passing it to the next handler in the chain.
Here’s the basic flow:
A request hits the server.
Middleware functions are executed in the order they were registered.
Each middleware can modify the request context, terminate the request, or pass it to the next handler.
The final handler (e.g., your route handler) processes the request and generates a response.
Chi provides built-in middleware for common tasks and makes it easy to write custom middleware.
Writing Middleware in Chi
Let’s dive into how to create and use middleware in Chi with a practical example.
Step 1: Setting Up Chi
First, install Chi:
go get github.com/go-chi/chi/v5
Here’s a basic Chi server setup:
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
})
http.ListenAndServe(":8080", r)
}
Step 2: Creating Custom Middleware
Middleware in Chi is a function that takes an http.Handler and returns an http.Handler. Here’s an example of a simple logging middleware that logs the request method and URL:
package main
import (
"log"
"net/http"
"github.com/go-chi/chi/v5"
)
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("Request: %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
func main() {
r := chi.NewRouter()
r.Use(LoggingMiddleware)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
})
http.ListenAndServe(":8080", r)
}
In this example:
LoggingMiddleware logs the request method and URL path.
The next.ServeHTTP(w, r) call passes control to the next handler in the chain.
r.Use(LoggingMiddleware) applies the middleware to all routes defined in the router.
When you run this code and visit http://localhost:8080, you’ll see a log like:
2025/05/24 18:40:00 Request: GET /
Step 3: Using Chi’s Built-in Middleware
Chi comes with several built-in middleware functions for common tasks. For example, you can use chi.Logger for request logging or chi.Recoverer to catch panics. Here’s how to use them:
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)
}
The middleware.Logger outputs detailed logs for each request, including status codes and response times.
Step 4: Applying Middleware to Specific Routes
You can apply middleware to specific routes or groups of routes using Chi’s Group or Route methods. For example, let’s apply an authentication middleware only to a protected route:
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "valid-token" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, ProtectedHandler)
})
}
func ProtectedHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Welcome to the protected route!"))
}
func main() {
r := chi.NewRouter()
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Public route"))
})
r.Group(func(r chi.Router) {
r.Use(AuthMiddleware)
r.Get("/protected", ProtectedHandler)
})
http.ListenAndServe(":8080", r)
}
In this example:
The / route is public and doesn’t require authentication.
The /protected route requires a valid Authorization header.
The AuthMiddleware checks for a token and either allows or denies access.
Step 5: Working with Request Context
Middleware can also modify the request context to pass data to downstream handlers. Here’s an example that adds a user ID to the request context:
package main
import (
"context"
"net/http"
"github.com/go-chi/chi/v5"
)
func UserMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "userID", "12345")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func main() {
r := chi.NewRouter()
r.Use(UserMiddleware)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value("userID").(string)
w.Write([]byte("User ID: " + userID))
})
http.ListenAndServe(":8080", r)
}
Here, UserMiddleware adds a userID to the request context, which the route handler retrieves and uses.
Best Practices for Middleware in Chi
Keep Middleware Focused: Each middleware should handle a single responsibility (e.g., logging, authentication).
Order Matters: Middleware is executed in the order it’s registered. Place critical middleware (e.g., authentication) before others.
Use Context Sparingly: Only store necessary data in the request context to avoid clutter.
Handle Errors Gracefully: Use middleware like middleware.Recoverer to prevent crashes from panics.
Test Middleware: Write unit tests to ensure middleware behaves as expected, especially for authentication or validation logic.
Conclusion
Middleware in Go with Chi is a clean and flexible way to handle cross-cutting concerns in your web application. By leveraging Chi’s middleware system, you can modularize your code, improve maintainability, and build robust APIs. Whether you’re using built-in middleware like Logger or writing custom middleware for authentication, Chi makes it easy to integrate into your routes.
Try experimenting with Chi’s middleware in your next Go project, and let us know in the comments how you’re using it! For more details, check out the Chi documentation.
Subscribe to my newsletter
Read articles from Sachin Chavan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
