Mastering Middlewares in Golang

13 March 2022
Mastering Middlewares in Golang

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

  1. 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)
  2. Logging handler prints Before handler is executed , writes a response and logs the URL path.
  3. Now, the original handler is served.
  4. 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 :)

Paypal - Mohit Khare
Buy me a coffee