Writing Handsome Golang Middleware

Wiring http.Handlers with Google Go’s standard library is straightforward & clean.

http.Handle("/thing", func(w http.ResponseWriter, r *http.Request) {
‌ ‌ w.Write([]byte("got my thing"))
})

Chaining handler’s to create “middleware” isn’t quite as lovely. Your route definitions are difficult to read, and underneath the hood each “handler” is a function returning an http.Handler function.

http.Handle("/thing", acl.ValidToken(logger.RestApi(rate.Limiter(thing.Post))))

Fortunately we require very little excess to create a beautiful flow of middleware handlers.  Additionally there are third-party packages available that we’ll look at.

The Pursuit

My aim in this endeavor is to:

  • Create a clean route definition.
  • Be compatible with std router & popular ones like Gorilla MUX & HttpRouter.
  • Allow contextual info to ride the chain.
  • Write as little code as possible.
  • Be intuitive, don’t diverge.
  • Maybe introduce a common error handler.

jumbo_gopher-4bf98fbc72cc188289ba2b458d4ce680

In The Wild

There are some sweet packages available, but I think there is room for more of less. Less additional code, and less of a learning curve for those to follow.

Alice is a popular package, probably the one I would’ve chosen. I like the syntax, it reminds me of .js Promises but I turned away because it felt incompatible with my goal of being intuitive/straightforward. But I like what I see in the source code, and would revisit Alice. There is also Interpose, or Negroni, but they both took unconventional approaches that did not meet my set of criteria.

 

Cleaning Up

We want our route definitions to read easily. Instead of a nested method chain, we want to see a linear method chain, as close to router.Handler("GET", "/thing", first, second, last) as we can get. We don’t want to wrap the router, that’s too disruptive, we just want to drop in a slice of middleware that stop wherever we encounter an error. Easy.

package restiful
import "net/http"

type Handler func (w http.ResponseWriter, r *http.Request) (error)

func Handle(handlers ...Handler) (http.Handler) {
‌ ‌ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
‌ ‌ ‌ ‌ for _, handler := range handlers {
‌ ‌ ‌ ‌ ‌ ‌ err := handler(w, r)
‌ ‌ ‌ ‌ ‌ ‌ if err != nil {
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ w.Write([]byte(err.Error()))
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ return
‌ ‌ ‌ ‌ ‌ ‌ }
‌ ‌ ‌ ‌ }
‌ ‌ })
}

Having middleware control-flow managed by a single http.Handler takes away the need to pass around “next”, and provides an expectation that middleware only needs to return an error, else the flow shall continue. Who doesn’t love returning errors in Go? Try it out, to me it really just feels right. Our middleware handler interface is basically ServeHTTP with an error return, and our route handler is an http.Handler processing middleware handlers looking for an error. We can use any http.Request based context package (such as http://www.gorillatoolkit.org/pkg/context), to pass contextual info along the middleware chain.

router.Handler("POST", "/thing", restiful.Handle(
‌ ‌ acl.ValidToken,
‌ ‌ logger.RestApi,
‌ ‌ thing.Post,
))

Recap

  • Create a clean route definition.
  • Be compatible with std router & popular ones like Gorilla MUX & HttpRouter.*Our http.Handler is compatible, although each middleware isn’t. Oh well, we can wrap external middleware in a restiful.Handler.
  • Allow contextual info to ride the chain.
  • Write as little code as possible.
  • Be intuitive, don’t diverge.
  • Maybe introduce a common error handler.

Our route definitions are now easy to read, and feel intuitive plugging into standard & popular routing packages. Get the restiful middleware package on github.com/laicosly/restiful

A specialist in web-app development, I’m passionate about the underlying codebase powering the user experience of modern applications, RESTful APIs, user-friendly UI design, build systems, and git based project deployments. https://github.com/laicosly

Comments are closed.