Middleware in Go REST Servers
Middleware is a core pattern in Go REST servers, allowing you to wrap handlers with reusable logic for logging, panic recovery, and other cross-cutting concerns. This post builds on the standard-library task server from Part 1 of this series, exploring how to add middleware using plain net/http, the gorilla/mux router, and the Gin framework.
A Basic Logging Middleware
In the original task server, each handler began with a log.Printf call to log the incoming request. Middleware eliminates this duplication. Consider a simple logging middleware that records the request method and URI, and also measures how long the handler took to complete:
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
start := time.Now()
next.ServeHTTP(w, req)
log.Printf("%s %s %s", req.Method, req.RequestURI, time.Since(start))
})
}
To apply this middleware globally, wrap the mux with the Logging function in main:
func main() {
mux := http.NewServeMux()
server := NewTaskServer()
mux.HandleFunc("POST /task/", server.createTaskHandler)
mux.HandleFunc("GET /task/", server.getAllTasksHandler)
mux.HandleFunc("DELETE /task/", server.deleteAllTasksHandler)
mux.HandleFunc("GET /task/{id}/", server.getTaskHandler)
mux.HandleFunc("DELETE /task/{id}/", server.deleteTaskHandler)
mux.HandleFunc("GET /tag/{tag}/", server.tagHandler)
mux.HandleFunc("GET /due/{year}/{month}/{day}/", server.dueHandler)
handler := middleware.Logging(mux)
handler = middleware.PanicRecovery(handler)
log.Fatal(http.ListenAndServe("localhost:"+os.Getenv("SERVERPORT"), handler))
}
You can also attach middleware per-route. If you only want logging on server.tagHandler, for instance, wrap that handler individually:
func main() {
mux := http.NewServeMux()
server := NewTaskServer()
// ... other handlers as before
mux.Handle("/tag/", middleware.Logging(http.HandlerFunc(server.tagHandler)))
// ... other handlers as before
log.Fatal(http.ListenAndServe("localhost:"+os.Getenv("SERVERPORT"), mux))
}
When mixing global and per-route middleware, the execution order changes. With global middleware, the order is:
request --> [Logging] --> [Mux] --> [Handler]
With per-route middleware on /tag/, the order becomes:
request --> [Mux] --> [Logging] --> [tagHandler]
In this example, the relative order matters little, but with multiple kinds of middleware, tracking the order becomes important.
Adding Panic Recovery
By default, net/http recovers from panics by closing the client connection and logging the error. To return a proper HTTP 500 response instead, write a custom recovery middleware:
func PanicRecovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer func() {
if err := recover(); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Println(string(debug.Stack()))
}
}()
next.ServeHTTP(w, req)
})
}
This middleware wraps the handler in a defer that recovers from any panic, logs the stack trace, and writes an internal error response. With both pieces of middleware in place, main looks like this:
func main() {
mux := http.NewServeMux()
server := NewTaskServer()
// ... registered handlers, as before
handler := middleware.Logging(mux)
handler = middleware.PanicRecovery(handler)
log.Fatal(http.ListenAndServe("localhost:"+os.Getenv("SERVERPORT"), handler))
}
The execution order for each request is now:
request --> [Panic Recovery] --> [Logging] --> [Mux] --> [tagHandler]
As before, you can mix and match: apply PanicRecovery to select routes while keeping Logging global.
Middleware Chains
As chains grow, managing order by hand becomes tedious. Several packages, such as alice, offer a more ergonomic way to define and reuse middleware chains. Before adding such a dependency, weigh the benefit: if your chain is simple, hand-written code is fine. Router packages and frameworks often include their own middleware facilities, making an extra dependency unnecessary.
Middleware with gorilla/mux
The gorilla/mux router includes a Use(...) method for global middleware. The companion gorilla/handlers package ships ready-made middleware, including panic recovery and logging. The following example shows both in use:
func main() {
router := mux.NewRouter()
router.StrictSlash(true)
server := NewTaskServer()
router.HandleFunc("/task/", server.createTaskHandler).Methods("POST")
router.HandleFunc("/task/", server.getAllTasksHandler).Methods("GET")
router.HandleFunc("/task/", server.deleteAllTasksHandler).Methods("DELETE")
router.HandleFunc("/task/{id:[0-9]+}/", server.getTaskHandler).Methods("GET")
router.HandleFunc("/task/{id:[0-9]+}/", server.deleteTaskHandler).Methods("DELETE")
router.HandleFunc("/tag/{tag}/", server.tagHandler).Methods("GET")
router.HandleFunc("/due/{year:[0-9]+}/{month:[0-9]+}/{day:[0-9]+}/", server.dueHandler).Methods("GET")
// Set up logging and panic recovery middleware.
router.Use(func(h http.Handler) http.Handler {
return handlers.LoggingHandler(os.Stdout, h)
})
router.Use(handlers.RecoveryHandler(handlers.PrintRecoveryStack(true)))
log.Fatal(http.ListenAndServe("localhost:"+os.Getenv("SERVERPORT"), router))
}
The router.Use calls set up the middleware; a single Use call can accept any number of handlers. The RecoveryHandler middleware uses functional options for configuration, here enabling stack logging on panic. The LoggingHandler middleware takes an io.Writer and requires a small adapter to fit the router.Use signature.
For per-route middleware with gorilla/mux, you can wrap individual handlers as with the standard library, or use subrouters with their own Use calls. The subrouter approach is less awkward when routing is already factored that way.
Middleware with Gin
Gin's default instance, created with gin.Default(), already registers logging and panic recovery middleware. If you prefer to set these up manually, instantiate gin.New() (which registers nothing) and add middleware via Use:
func main() {
// Set up middleware for logging and panic recovery explicitly.
router := gin.New()
router.Use(gin.Logger())
router.Use(gin.Recovery())
server := NewTaskServer()
router.POST("/task/", server.createTaskHandler)
router.GET("/task/", server.getAllTasksHandler)
router.DELETE("/task/", server.deleteAllTasksHandler)
router.GET("/task/:id", server.getTaskHandler)
router.DELETE("/task/:id", server.deleteTaskHandler)
router.GET("/tag/:tag", server.tagHandler)
router.GET("/due/:year/:month/:day", server.dueHandler)
router.Run("localhost:" + os.Getenv("SERVERPORT"))
}
Gin middleware differs from the standard net/http signature. Its type is defined in the gin package as:
type HandlerFunc func(*Context)
Standard-signature middleware must be adapted to fit this shape. For additional middleware in Gin projects, the gin-contrib organization maintains a large collection of reusable modules.
Beyond Logging and Recovery
The examples above focus on the mechanism of middleware. In practice, the pattern supports a wide range of tasks: request validation, CORS handling, compression, sessions, tracing, caching, encryption, and authentication. Many of these will be explored later in this series, particularly authentication.
A Note on Overuse
Middleware complicates the flow of a request, which can make reading and debugging code harder. Use the pattern judiciously. Define all middleware in one location, and avoid dynamically layering it onto routes conditionally or within other middleware. A little discipline now saves significant debugging effort later.



