Middleware in Go

Middleware in Go

When building an application, a middleware is a code that hooks on the server-based request/response lifecycle, which will then chain the request from the client to the next middleware function and eventually the last function.

This article thoroughly explores basic middleware chaining/multiple middlewares.

What is Middleware

A middleware is an http.handler that wraps another http.handler in a server request/response processing. Middleware can be defined in many components; It sits between the web server and the actual handler. Whenever a handler is defined for a URL pattern, the request hits the handler and executes the business logic. All middleware process these functions:

  • Process the request from the client before hitting the handler(authentication)

  • Process the handler function

  • Process the response for the client

  • logging.. and so on

In an application with no middleware, if a client sends a request, the request reaches the server and is handled by some function handler, and it is sent back immediately from the server to the client. But in an application with middleware, the request made by the client passes through stages like logging, authenticating, session validation, and so on, then process the business logic. It filters wrong requests from interacting with the business logic.

Basic Middleware

The principle of closure function helps us write middleware. A closure function returns another function. Let's write a basic route handler with the name basicMiddleware.go

 package main
    import (
        "fmt"
        "net/http"
    )
    func logic(w http.ResponseWriter, r *http.Request) {
        fmt.Println("Execute the logic")
        w.Write([]byte("OK!!!"))
    }
    func main() {
        handlerLogic := http.HandlerFunc(logic)
        http.Handle("/", middleware(handlerLogic))
        http.ListenAndServe(":9000", nil)
    }
    func middleware(handler http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            fmt.Println("Middleware execution before request")
            w.Write([]byte("Response in middleware "))
            handler.ServeHTTP(w, r)
            fmt.Println("Middleware execution after response")
        })
    }

When you run the code above with go run basicMiddleware.go and check http://localhost:9000 in your browser for this response.

Response in middleware OK!!!

The console will receive this message:

Middleware execution after response Middleware execution before request Execute the logic Middleware execution after response

Explanation: The function takes in a handler and creates a wrapper to execute some operations before and after calling the handler.

  • The request comes on a route(middleware).

  • The middleware handler prints Middleware execution after response, writes a response, and logs the URL path.

  • Serves the handler, which was passed an argument.

  • Post execution, the middleware function returns back the handler function to the route.

A Real-world Example of Middleware with Multiple and Chaining Middleware

It is possible to chain a group of middleware with the closure logic we used in the basic. Let us create a bookstore API for saving book details; the API will have one POST method and three fields: book name, the year it was published, and the book's author. In a scenario, an API developer only allows JSON-type media from the client and must send the server time in UTC back to the client for every request. Using middleware to do this. It takes two functions of middleware, which are:

  • The first function. Check if the content type is JSON. if not, it won’t allow the request to proceed.

  • The second function adds a Server-Time(UTC) timestamp to the response cookie.

Let's build our project by creating a file name bookMiddlewareAPI.go or anything you want to name.

package main
import (
    "encoding/json"
    "fmt"
    "net/http"
)
type bookstore struct {
    Name   string
    Year   uint64
    Author string
}
func bookLogic(w http.ResponseWriter, r *http.Request) {
    if r.Method == "POST" {
        var book bookstore
        decoder := json.NewDecoder(r.Body)
        err := decoder.Decode(&book)
        if err != nil {
            panic(err)
        }
        defer r.Body.Close()
        fmt.Printf("God a book %s, that was published in %d. The author of the book is %s\n", book.Name, book.Year, book.Author)
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("201 - Created"))
    } else {
        w.WriteHeader(http.StatusMethodNotAllowed)
        w.Write([]byte("405 - Method Not Allowed"))
    }
}
func main() {
    http.HandleFunc("/bookstore", bookLogic)
    http.ListenAndServe(":9000", nil)
}

You run the code above in your terminal:

go run bookMiddlewareApi.go

Then give a CURL request in another terminal:

 curl -H "Content-Type: application/json" -X POST http://localhost:9000/bookstore -d '{"name":"Lord of the Rings", "year": 1957, "author": "J.R.R.Tolkien"}'

Go gives you the following:

God a book Lord of the Rings, and was published in 1957. The author of the book is J.R.R.Tolkien

CURL response will be:

201 - Created

Explanation: An API with a POST was created as the allowed method. it will store data in a database. JSON package was imported and used to decode the POST body given by the client. A structure was created to map the JSON body, and JSON got decoded and printed information to the console.

It needs to pass a handler between multiple middleware to chain middleware together, with only one handler involved in the preceding example. and the idea is to pass the main handler to multiple middleware handlers. we’ll complete the code.

package main
import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "time"
)
type bookstore struct {
    Name   string
    Year   uint64
    Author string
}
func bookLogic(w http.ResponseWriter, r *http.Request) {
    if r.Method == "POST" {
        var book bookstore
        decoder := json.NewDecoder(r.Body)
        err := decoder.Decode(&book)
        if err != nil {
            panic(err)
        }
        defer r.Body.Close()
        fmt.Printf("God a book %s, and was published in %d. The author of the book is %s\n", book.Name, book.Year, book.Author)
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("201 - Created"))
    } else {
        w.WriteHeader(http.StatusMethodNotAllowed)
        w.Write([]byte("405 - Method Not Allowed"))
    }
}
func checkContentType(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Println("Checking the content type middleware")
        if r.Header.Get("Content-Type") != "application/json" {
            w.WriteHeader(http.StatusUnsupportedMediaType)
            w.Write([]byte("415 - Unsupported Media Type, Please send a JSON"))
            return
        }
        handler.ServeHTTP(w, r)
    })
}
func cookieTimer(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        handler.ServeHTTP(w, r)
        cookies := http.Cookie{Name: "Server-Time(UTC)", Value: strconv.FormatInt(time.Now().Unix(), 10)}
        http.SetCookie(w, &cookies)
        log.Println("Currently in the set server time middleware")
    })
}
func main() {
    bookLogicHandler := http.HandlerFunc(bookLogic)
    http.Handle("/bookstore", checkContentType(cookieTimer(bookLogicHandler)))
    http.ListenAndServe(":9000", nil)
}

You run the code above in your terminal:

go run bookMiddlewareApi.go

Then give a CURL request in another terminal:

curl -i -H "Content-Type: application/json" -X POST http://localhost:9000/bookstore -d '{"name":"Lord of the Rings", "year": 1957, "author": "J.R.R.Tolkien"}'

The output looks like this:

   HTTP/1.1 200 OK
   Date: Tue, 06 Jun 2023 21:56:51 GMT
   Content-Length: 13
   Content-Type: text/plain; charset=utf-8

   201 - Created

If we remove the Content-Type:application/json from the CURL command, middleware blocks us from executing the main handler:

curl -i -X POST http://localhost:9000/bookstore -d '{"name":"Lord of the Rings", "year": 1957, "author": "J.R.R.Tolkien"}'

The output looks like this:

  HTTP/1.1 415 Unsupported Media Type
  Date: Tue, 06 Jun 2023 22:00:11 GMT
  Content-Length: 46
  Content-Type: text/plain; charset=utf-8

  415 - Unsupported Media Type, Please send a JSON

Cookies will be set from the other middleware.

Explanation: checkContentType is the first middleware we added to the preceded code; it checks the content type request and allows or blocks the request from going further. The cookieTimer is the second middleware we added; it was designed to add a cookie to the response with a value of the time in the UNIX epoch. The form of chaining middleware we did is readable for two to three middleware:

func main() {

    bookLogicHandler := http.HandlerFunc(bookLogic)

    http.Handle("/bookstore", checkContentType(cookieTimer(bookLogicHandler)))

    http.ListenAndServe(":9000", nil)

}

Conclusion

This article discussed some essential aspects of building web applications with Go. It is important for a Go developer to know middleware and how to use it. I hope you’ve learned a lot from this article. Feel free to leave a comment on what is not clear to you.