DEV Community

elesq
elesq

Posted on

Custom middleware basics with Go. No frameworks.

So you want to add some custom middleware to a Go api? There's many elegant and advanced ways to do this, which is great! - but who wants to simply use a pre-cooked black-box when you can cut your own and have some sort of idea how others are doing it?

Here we're going to demo some fairly trivial hand-rolled middleware functions that will wrap the original function handling our request.

  1. Let's create a fairly standard looking function that takes a ResponseWriter and a Request.
func postHandler(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"))
    }
}
Enter fullscreen mode Exit fullscreen mode

Thing to note:

  • we can use the http types instead of r.Method == "POST" for better clarity.
  • In the snippet above you can see we mention the type we create called Lesson, you will see this depicted in the snippet that shows the package, imports and the main function below.

Adding Middleware

So, now the middleware we'd like to apply is firstly to check that we're accepting JSON format, or prevent the request from going anywhere. We can do that like this.

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 is expected"))
            return
        }

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

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

Things to note:

  • we take an http.Handler and return an http.Handler
  • we can do stuff before and after handling the request. Here we're doing a but of useless printing but you get the idea.

The next thing we want to do is apply another middleware and this time we're going to add a cookie attribute with the server timestamp. This magic can be woven like so...

unc 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")
    })
}
Enter fullscreen mode Exit fullscreen mode

Things to note:

  • again you can see we can have a before and after routine demonstrating we have standard wrapping capabilities yet again.
  • we create and set the cookie value using an http in-built method.

Testing our work

OK, so no we need a way to run this so we can flesh out our main method like this.

 package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "time"
)

type Lesson struct {
    Title   string
    Summary string
}

func main() {
    port := ":8080"

    originalHandler := http.HandlerFunc(postHandler)
    http.Handle("/lesson", filterContentType(setServerTimeCookie(originalHandler)))

    //http.HandleFunc("/city", poastHandler)
    log.Fatal(http.ListenAndServe(port, nil))
}
Enter fullscreen mode Exit fullscreen mode

things to note here:

  • the originalHandler is an http.HandlerFunc type.
  • you can see that each middleware effectively wraps what it receives, this allows us to chain the middlware functions around the original handler meaning we can have a a line like filterContentType(setServerTimeCookie(originalHandler))
  • we could even do it on a one liner, but clarity starts to dwindle. If you want to try it swap to http.Handle("/lesson", filterContentType(setServerTimeCookie(http.HandlerFunc(postHandler))))

so, now we're going to need some positive and negative tests. To do so easily we can manually test on the CLI using curl.

So, let's see if we can process non-json input as the request.

curl http://localhost:8080/city -d '{"title": "middleware with Go", "summary": "learn some middleware concepts with a quick and dirty demo"}'
Enter fullscreen mode Exit fullscreen mode

like me, you should be met with a 415 - Unsupported Media Type. JSON type expected% response to your test.

Now let's try what should hopefully be a successful attempt.

curl -i -H "Content-Type: application/json" -X POST http://localhost:8080/city -d '{"title": "Middleware demo with Go", "summary": "Hopefully a successful demo now that we have specified mime type and the method"}'

Hopefully your output is similar to below. 

`HTTP/1.1 200 OK
Date: Tue, 27 Jul 2021 20:09:52 GMT
Content-Length: 13
Content-Type: text/plain; charset=utf-8

201 - Created%`
Enter fullscreen mode Exit fullscreen mode

That wraps up the post. I hope you found this quick overview to be of some use if you too are messing about with Go and looking to grasp what middleware is and how it works.

Best wishes

Discussion (0)