Mastering Middlewares in Golang
Get latest articles directly in your inbox
Middleware is a piece of code that executes on requests from a client to perfrom some operation before/after the server processes the request and serves the response back to client. The request is chained from one middle to another until the final executing handler processes it. A common use case of middleware is to server a logic that is common across multiple endpoints (remember DRY principle).
Some common examples for middlewares are:
- logging
- authentication
- data collection
- recovery
A middleware can do validations on the request and add/update the response before it is passed onto the client. In this article we will learn how to write a middleware in golang. We’ll also explore how to chain multiple middlewares on a single route. Let’s get started!
Do explore articles on Golang and System Design. You’ll learn something new 💡
Basic Middleware
Creating a route
To actually use a middleware we first need a basic route handler. Let’s create a simple route and a handler for this.
package main
import (
"fmt"
"log"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Prinln("Inside the handler")
w.Write([]byte("OK - executing handler"))
}
func main() {
http.Handle("/", handler)
http.ListenAndServe(":8000", nil)
}
Make a GET call to http://localhost:8000/ and you should see this response.
OK - executing handler
Building the Middleware
Now that we have a route ready, let’s create a simple logging middleware.
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Before handler is executed")
w.Write([]byte("Adding response via middleware\n"))
log.Println(r.URL.Path)
next.ServeHTTP(w, r)
fmt.Println("After handler is executed")
})
}
Explanation
What we’ve done here is to define a new function that takes in our original handler and creates a wrapper on it to execute some operations before and after calling the original handler.
Here’s how the flow works
- Request comes on a route. Since route has a middleware to it
loggingMiddleware
is called. (Don’t worrry in next section we’ll cover how to add middleware to your routes) - Logging handler prints
Before handler is executed
, writes a response and logs the URL path. - Now, the original handler is served.
- Post execution, the middleware function returns back the
HandlerFunc
to the route.
Setting up Middleware
Now that we have our middleware function ready to be used, let’s add it to our defined route. Here’s the complete program - notice the change where we define our route.
package main
import (
"fmt"
"log"
"net/http"
)
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Before handler is executed")
w.Write([]byte("Adding response via middleware\n"))
log.Println(r.URL.Path)
next.ServeHTTP(w, r)
fmt.Println("After handler is executed")
})
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Println("Inside the handler")
_, err := w.Write([]byte("OK - executing handler"))
if err != nil {
fmt.Println("Error writing response")
}
}
func main() {
http.Handle("/", loggingMiddleware(http.HandlerFunc(handler))) // --> adding middleware to route
err := http.ListenAndServe(":8000", nil)
if err != nil {
fmt.Println("Error: setting up server")
}
}
Let’s try to make a GET call on http://localhost:8080/ . We should see the middleware print statements and logs in your terminal. The response would also have Adding response via middleware
. Congrats! You just wrote a working middleware in Go.
Response:
Adding response via middleware
OK - executing handler
Server output
Before handler is executed
2022/03/13 12:11:22 / --> url path with timestamp
Inside the handler
After handler is executed
Advanced middlewares
In real world middlewares are used quite extensively and serve a variety of purposes. Sharing some of the common middlewares that can come in handy during API development.
Adding Headers in Middleware
Headers are an important part of REST APIs. Middleware can be used to add common headers across routes.
func headerMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
next(w, r)
}
}
Recovery Middleware
Panics can happen anytime due to crashes, memory leak, etc. The server should be able to recover from such panics. Golang provides us with recover()
function i.e a built-in function that regains control of a panicking goroutine.
func panicRecovery(next http.HandlerFunc) http.HandlerFunc {
return 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(w, req)
}
}
CORS Middleware
You don’t want your APIs to be accessible to request from another domain. To enhance security we can use a CORS middleware on the public routes.
func CORSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
w.Header().Set("Access-Control-Allow-Origin", origin)
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Methods", "GET,POST")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-CSRF-Token, Authorization")
return
} else {
next.ServeHTTP(w, r)
}
})
}
Chaining Middlewares
In the previous section we defined couple of middleware. Usually you would want to use more than one middleware on a route like Logging, panic, authorization, etc. Go provides us a way to chain multiple middlewares.
func main() {
http.Handle("/",
CORSMiddleware(
panicRecovery(
headerMiddleware(
loggingMiddleware(http.HandlerFunc(handler))))))
err := http.ListenAndServe(":8000", nil)
if err != nil {
fmt.Println("Error: setting up server")
}
}
You see the problem here? With a lot of middlewares the code is getting ugly. To solve this we can create a slice of Middleware
type and iterate over this slice for each route.
type Middleware func(http.Handler) http.Handler
func chainingMiddleware(h http.Handler, m ...Middleware) http.Handler {
if len(m) < 1 {
return h
}
wrappedHandler := h
for i := len(m) - 1; i >= 0; i-- {
wrappedHandler = m[i](wrappedHandler)
}
return wrappedHandler
}
func main() {
commonMiddlewares := []Middleware{
CORSMiddleware,
panicRecovery,
headerMiddleware,
loggingMiddleware,
}
http.Handle("/", chainingMiddleware(http.HandlerFunc(handler), commonMiddlewares...))
err := http.ListenAndServe(":8000", nil)
if err != nil {
fmt.Println("Error: setting up server")
}
}
Resources
Books to learn Golang
Liked the article? Consider supporting me ☕️
I hope you learned something new. Feel free to suggest improvements ✔️
I share regular updates and resources on Twitter. Let’s connect!
Keep exploring 🔎 Keep learning 🚀
Liked the content? Do support :)