Chaining middleware in Go using justinas/alice package

We can make a couple of improvements to the simple API we created that included a couple of middleware wrapping functions.

  1. We're going to split the project into separate files.
  2. We're going to use a 3rd party package to chain the middleware functions using a popular community package called alice.

Let's go

  1. create a new project and run go mid init <name-of-your-project>
  2. Run go get github.com/justinas/alice on the cli.
  3. Create main.go, handlers.go & middlewares.go files.

add our handler

In the handlers.go file we want to place the following code.

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

type Lesson struct {
    Title   string
    Summary string
}

func handle(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodPost {

        // create a variable of our defined struct type Lesson
        var tempLesson Lesson

        decoder := json.NewDecoder(r.Body)
        err := decoder.Decode(&tempLesson)
        if err != nil {
            panic(err)
        }

        defer r.Body.Close()

        fmt.Printf("Title: %s. Summary: %s\n", tempLesson.Title, tempLesson.Summary)
        w.WriteHeader(http.StatusCreated)
        w.Write([]byte("201 - Created"))

    } else {
        w.WriteHeader(http.StatusMethodNotAllowed)
        w.Write([]byte("405 - Method Not Allowed"))
    }
}

Add the middleware functions

In the middlewares.go file we can add the following code.

package main

import (
    "log"
    "net/http"
    "strconv"
    "time"
)

func filterContentType(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Println("Inside filterContentType middleware, before the request is handled")

        // filter requests by mime type
        if r.Header.Get("Content-Type") != "application/json" {
            w.WriteHeader(http.StatusUnsupportedMediaType)
            w.Write([]byte("415 - Unsupported Media Type. JSON type expected"))
            return
        }

        // handle the request
        handler.ServeHTTP(w, r)

        log.Println("Inside filterContentType middleware, after the request was handled")
    })
}

func setServerTimeCookie(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Println("Inside setServerTimeCookie middleware, before the request is handled")

        // handles the request
        handler.ServeHTTP(w, r)

        cookie := http.Cookie{Name: "Server Time(UTC)", Value: strconv.FormatInt(time.Now().Unix(), 10)}
        http.SetCookie(w, &cookie)

        log.Println("Inside setServerTimeCookie middleware, after the request is handled")
    })
}

add the main function

in main.go we need to add the following

package main

// chaing middleware with alice.
// here we will use the alice package to demo chainging middleware
// around our original or main handler.

import (
    "log"
    "net/http"

    "github.com/justinas/alice"
)

func main() {

    port := ":8080"

    originalHandler := http.HandlerFunc(handle)
    chain := alice.New(filterContentType, setServerTimeCookie).Then(originalHandler)

    http.Handle("/lesson", chain)
    log.Fatal(http.ListenAndServe(port, nil))
}

As you can see our refactor has added a 3rd party dependency but increased readbility. The fluent API of the alice package makes for clear and readable code when used.

Splitting the project into multiple files here is trivial as we have a tiny codebase and it is purely for demo purposes, however it is reasonably good, common practice to split your files in Go projects. This lowers the cognitive load from each file on the reader.

28