HTTP middlewares provide an elegant way to inspect, check or move incoming requests away from the application layer.
Middlewares could be used for various reasons: logging, authentication, caching... you name it.
In this article, we'll implement two simple yet very useful middlewares:
Logging middleware to log information about incoming requests.
A middleware to check if the incoming request has a valid content-type.
HTTP Server in GO
Before implementing any of our middlewares, it makes sense to write a simple HTTP server first.
Using Go's net/http, a simple HTTP server implementation might look like this:
package main
import (
"fmt"
"log"
"net/http"
)
func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Howdy Gophers 👋\n")
}
func main() {
port := 5000
http.HandleFunc("/", home)
log.Printf("Running on http://127.0.0.1:%d 🚀🚀", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
}
This will spin up an HTTP server listening for requests on port 5000
Run the program using a simple "go run
" command:
$ go run main.go
Testing the server with curl

Great, our server is responsive.
Logging middleware
The logging middleware should intercept an incoming request, grab some information about it and then log it to the console in the desired format.
If you check the server's console after sending the previous request, you may see something like this:

All quite, nothing interesting regarding our request. Wouldn't it be better if we print something to the console after each request?
You might want to implement this kind of logic at the top of the "home"
handler, which is totally fine, but imagine if our application handles too many endpoints, it would make more sense to write this logic in a centralized reusable function instead of writing the same block of code over and over which could lead to bugs, moreover, it would be a pain if, for example, we wanted to change the logging format.
Well, this is pretty much the idea behind middlewares, they are just functions that act as a middleman between the request and the endpoint's handler, in the case of HTTP middlewares, the only extra thing is that those functions should return an http.HandlerFunc function.
With all of this being said, a typical middleware would look like this:
func logger(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
/*
executes before serving the response to the client
*/
log.Println("Request received")
next.ServeHTTP(w, r)
log.Println("Response sent")
/*
executes after serving the response to the client
*/
}
}
After applying this middleware to the home handler: http.HandleFunc("/", logger(home))
Here's how our server's console looks like after making those changes:

Now, our server outputs some information whenever it receives a request.
We call the "next.ServeHTTP"
to pass the ResponseWriter and the Request objects to the next middleware in the chain, which in our case, is nothing but the "home
" handler.
Let's update our logger middleware to output the following format:
HTTP_METHOD /PATH REMOTE_ADDR
example:
GET / 127.0.0.1
func logger(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
method := r.Method
path := r.URL.Path
remoteAddr := strings.Split(r.RemoteAddr, ":")[0]
log.Printf("%s %s %s\n", method, path, remoteAddr)
next.ServeHTTP(w, r)
}
}
Everything related to the incoming request is accessible from the http.Request object so it's definitely interesting to check it out.
Restart the server and send a new request:

Great, our server now gives insights on each incoming request.
The content-type middleware
REST APIs often have endpoints that accept only JSON inputs, so it would be convenient to implement a middleware that checks if the incoming request has the Content-Type set to application/json and returns a 405 status code otherwise.
Just like in the previous example, this middleware will be just a function that takes an http.HandlerFunc and returns a new one.
func isJSON(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
http.Error(w, "content-type header must be application/json", http.StatusBadRequest)
return
}
next.ServeHTTP(w, r)
}
}
After applying this middleware to the home handler: http.HandleFunc("/", logger(isJSON(home)))
Restart the server and send a request with a content-type header set to something different than application/json:

As you can see the request wasn't allowed to access the "home
" handler because it was stopped by the "isJSON
" middleware since it doesn't have a content-type header set to application/json.
Let's now send a new request with a content-type header set properly:

Of course, it makes no sense to set the content-type header in a GET request, but for the sake of the tutorial, let forget about it.
As you can see, we receive now the response that was actually sent by the "home
" handler because the "isJSON"
middleware didn't stop our request and return the response we received previously, since the content-type was set to the value that we expect.
Congrats, you know now how to write your own custom HTTP middlewares, which will definitely prove handy in your next applications.