Lot of the tutorials on the Web show different ways of writing Go HTTP middlewares, but most of them use functional approach meaning using functions that get dependencies as parameters and return HTTP handler function or handler. There is nothing wrong about this, but it can be quite messy to write a complex middlewares like database based authentication and authorization or simply middlewares which have lot of dependencies. In this article I am going to explain how to use more object oriented way of writing such middlewares. Let’s start with the basic object oriented middleware example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type Middleware struct {
	next http.Handler
	// other dependencies
}

func NewMiddleware() *Middleware {
	return &Middleware{}
}

// Handle wraps handler. Can be called just once.
func (m *Middleware) Handle(next http.Handler) http.Handler {
	m.next = next

	return m
}

// ServeHTTP implements http.Handler interface.
func (m *Middleware) ServeHTTP(res http.ResponseWriter, req *http.Request) {
	res.Write([]byte("do something before next\n"))
	m.next.ServeHTTP(res, req)
	res.Write([]byte("do something after next\n"))
}

// Wrap wraps handler function. Can be called just once.
func (m *Middleware) Wrap(next http.HandlerFunc) http.HandlerFunc {
	m.next = next

	return m.ServeHTTP
}

Here we can use NewMiddleware constructor to pass all dependencies like database connection pool and so on. We can use returning middleware object in several different ways. Now let’s check how we can use this middleware.

1. Wrap single endpoint

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main() {
	welcome := func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("welcome\n"))
	}

	m := NewMiddleware()

	http.HandleFunc("/", m.Wrap(welcome))
	http.ListenAndServe(":3000", nil)
}

This method is intended to be used just to wrap a single handler function. If you use it multiple times on the same middleware instance, it may produce unwanted results because each call to Wrap method would override the next handler function. If you have to wrap multiple functions, please check the examples number 3, 4 and 5.

2. Handle single endpoint

1
2
3
4
5
6
7
8
	welcome := func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("welcome\n"))
	}

	m := NewMiddleware()

	http.Handle("/", m.Handle(http.HandlerFunc(welcome)))
	http.ListenAndServe(":3000", nil)

In this example you can see how to use Handle method. Again, this can be used just once similar as noted in the previous example.

3. Handle multiple endpoints

It this example we are going to use Handle method to wrap calls to all available endpoints.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
	welcome := func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("welcome\n"))
	}

	m := NewMiddleware()
	mux := http.NewServeMux()

	mux.HandleFunc("/", welcome)
	http.ListenAndServe(":3000", m.Handle(mux))
}

As you can see, we wrap the whole multiplexer in this example so all handler functions (http.HandleFunc) and handlers (http.Handle) would be wrapped in the middleware. This is probably the most useful example so far and it is intended to be used for authentication, authorization and similar middlewares which are gonna be used by multiple endpoints.

4. Wrap multiple sub endpoints

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
	welcome := func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("welcome\n"))
	}

	m := NewMiddleware()
	mux := http.NewServeMux()
	subMux := http.NewServeMux()

	mux.HandleFunc("/", welcome)
	subMux.Handle("/welcome", m.Wrap(welcome))
	mux.Handle("/welcome", subMux)

	http.ListenAndServe(":3000", mux)
}

Here we have an example where / endpoint is not wrapped by middleware and /welcome endpoint is wrapped. Just take care that you call Wrap of Handle just once on one instance of the middleware.

5. Handle with Chi

As Chi is Go idiomatic router, I would like to show you how you can use this pattern with it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
	welcome := func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("welcome\n"))
	}

	r := chi.NewRouter()
	m := NewMiddleware()

	r.Use(m.Handle)
	r.Get("/", welcome)
	http.ListenAndServe(":3000", r)
}

Similar as in the example 3, we use Handle method to apply the middleware to all further endpoints.

Once more, take care that multiple calls to Handle or Wrap methods on the same instance of the middleware may produce unpredicted behavior because you may override the next handler. Of course, you can make your Handle and Wrap methods panic if next is already set in order to avoid confusion.