This is the second in my series of posts showcasing how to use the “adapter pattern” in Go. If you missed Part 1, be sure to check it out because we’re going to be building on that here. For this post I’m going to show how you can adapt routes on your HTTP server with middleware functions. The inspiration for this post comes from Mat Ryer’s middleware post here.

When you’re building an HTTP server it’s nice to wrap your routes in middleware that perform repetitive operations (e.g., validation, authentication, logging, etc.). That way, you don’t have that logic cluttering up your handler implementations and instead you have clean, modular components that are easily testable.

Before we dive in, I want to point out a neat bit of code in the Go net/http standard library:

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

http.HandlerFunc implements the http.Handler interface, which means we can use regular old functions as HTTP handlers. This is pretty convenient for what we’re about to discuss.

With that out of the way, lets consider the following Adapter type:

type Adapter func(http.Handler) http.Handler

This is just a function that accepts an http.Handler and returns an http.Handler; simple enough. This is actually quite powerful though, especially when you consider writing functions that return an adapter:

func LogBeforeAfter() Adapter {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            log.Println("before")
            defer log.Println("after")
            next.ServeHTTP(w, r)
        }
    }
}

This will write log statements before and after calling the wrapped handler. What’s cool is that we can leverage this closure to pass in arguments that give state to our middleware:

func RequireMinFoo(minFoo int) Adapter {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            foo := r.Context().Value("foo")
            if foo == nil {
                // handle missing foo
                return
            }
            if foo.(int) < minFoo {
                // handle small foo
                return
            }
            next.ServeHTTP(w, r)
        }
    }
}

This is very convenient because we can use the same middleware logic but wrap different routes/handlers with different requirements for minFoo (you’ll see what I mean in a second).

In order to tie everything together, we can implement a function called Adapt that leverages the variadic options from Part 1 of this series:

func Adapt(h http.Handler, adapters ...Adapter) http.Handler {
    for i := range adapters {
		adapter := adapters[len(adapter)-1-i] // range over in reverse
		h = adapter(h)
    }
    return h
}

Note that there’s a little subtlety here: we’re ranging over and applying the supplied adapters in reverse. This is largely for readability convenience when we’re constructing our routes. For the following example, assume there’s some service instance s that has an associated router (typically I use gorilla/mux) and some handler functions handleFooGet handleFooDelete. We can construct some routes like:

s.router.Handle("/foo/{id}", adaptHandler(
    s.handleFooGet(),
    setContentType("application/json"),
    s.parseClaimsToContext(),
    hasAtLeastOnePrivilege("ordinary", "admin", "super"),
)).Methods("GET")
s.router.Handle("/foo/{id}", adaptHandler(
    s.handleFooDelete(),
    setContentType("application/json"),
    s.parseClaimsToContext(),
    hasAtLeastOnePrivilege("admin", "super"),
)).Methods("DELETE")

The first argument is the handler function itself, then each middleware function runs in order. For the GET route, the content type is set to application/json, a function defined on service is used to populate some request Context data, and finally a middleware requires the caller’s privileges to satisfy some criteria. The DELETE route is similar, but you can see that we’ve supplied different args for the privilege middleware. This is a really nice approach to middleware because now they’re modular, easily testable, and we can see exactly how a request/response flows through to our endpoint.

That’s it for this component of the series. In part 3 I’m going to veer off course a bit and show how I like to implement the handlers on my service; we’ll take a deeper look into how s.handleFooGet and s.handleFooDelete are implemented.