DEV Community

Cover image for A mini-guide — Middleware, and how it works in Go
Johan Lejdung
Johan Lejdung

Posted on • Updated on

A mini-guide — Middleware, and how it works in Go

In this article I will explain the concept of middleware and create a working example in Go. The code for the complete project will be linked at the end of the article.

The article is written as a companion piece to my previous article A mini-guide — Build a REST API as a Go microservice together with MySQL. Although this article is standalone and will be readable and understandable without having read the other one beforehand.

This code is using Go Modules, if you are unfamiliar with Go Modules read about it in this other article of mine.

Let’s dive in! <⌨>️

Middleware, what even are they?

If you are already familiar with Middleware and just want to get to the action, you can skip this section.

Middleware, what even are they?

If you feel like Ryan Reynolds above when I mention Middleware, I got you covered 🤗

In the world of networking a middleware is code that executes on network-based requests made by a client, which will then chain the request to the next middleware and eventually the final executing function. This is useful when you want to separate operations shared across multiple endpoints, such as: authentication, performance logging and data collection.

As an example; let’s say we have an endpoint that returns a list of messages posted by a user, and another that returns sensitive personal information about that user.

Both endpoints are only accessible when the user is logged into the service. Additionally, we also want to log the execution time of each request made to our server. The code for the authentication and logging could be written together with the controller and copied over to the two endpoints. But the example is easily split into four different packages.

One that logs data about the request (LOG), one that verifies that the user is logged in (AUTH), one that returns a list of messages (RETURN MSG) and one that returns user data (RETURN USER).

By writing these functionalities as middleware we can easily chain the request and return prematurely at each middleware if needed, for example if the user is not logged in whilst making the request.

It might be chained as this:

Enter fullscreen mode Exit fullscreen mode

With the logic for LOG and AUTH separated, we only have to write the code and tests for it once. 🎉

Middleware in Go

In Go all of the network requests are fulfilling an interface called Handler in the net/http package. For those unfamiliar with Go, a handler responds to a HTTP request and handles both reads from the request and writes to the response.

Because Go comes with and expects this Handler interface in it’s most crucial functions, it’s very easy and reliable to build packages around it and extend the interface with wrappers, such as middleware.

A quick way to create a middleware would be to wrap the net/http.Handler.ServeHTTP method;

ServeHTTP(ResponseWriter, *Request)
Enter fullscreen mode Exit fullscreen mode

by adding a outer function that takes a Handler as a parameter, as well as returning a Handler which in this case takes the form of a HandlerFunc.

The function above takes an argument of type Handler, which is used to chain the request at the end of the method. Because we also return a Handler type, we can chain the calls. It could look as such;

Enter fullscreen mode Exit fullscreen mode

From this simple method alone we can make a pretty good implementation of middleware. It might not be as robust as we would like, but it’s doable.

Going forward I will use a package called Negroni that makes this process a whole lot easier, although feel free to roll your own implementation. 👍

Negroni wraps around your router of choice and provides functionality for middleware management. Such as a simpler, and more dynamic way of chaining middleware.

Implementation — Wrapping the Handler

In order to keep it as simple as possible, I’m planning on implementing a middleware that will log the execution time of a network request. The middleware will be built and executed with Negroni.

Let’s start with creating our router and wrapping it in Negroni!

As seen in the code sample above; we start out, on line 12, with creating a new router with the gorilla/mux package, a popularly used package for router management. On the next few rows we register our endpoint, a GET endpoint on the root path /. So far, so good, and some of you will have seen this exact code before!

On line 18 something new happens, we create a new instance of a Negroni handler, on line 19 we tell the instance to use the router that we previously created.

Because Negroni fulfils the Handler interface, we can use it just as we would have used the router. Which we do on line 21, we pass in the negroni handler in place of where we would normally pass in the mux router. If you execute this code and call the endpoint you observe that it works as expected, eg. it logs “Endpoint handler called” when calling:

curl localhost:8080/
Enter fullscreen mode Exit fullscreen mode

Implementation — The birth of a Middleware

Okay! So we have Negroni and an endpoint setup, let's continue with the middleware creation!

As previously explained, a middleware is really just a Handler. So all we need to do is make a struct that fulfils the interface of a Handler. The twist here is that since we are using Negroni we have to implement their version of the Handler interface.

As seen below it’s similar to the net/http.Handler interface, except that it also has support for built in chaining with the next parameter.

type Handler interface {
    ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc)
Enter fullscreen mode Exit fullscreen mode

Let’s create a simple logger.

The code snippet above shows how you could create a really simple middleware by fulfilling the negroni.Handler interface. As seen, on line 12 we log some arbitrary information. On the next line we save the current timestamp to the variable t.

Finally; on line 14 we continue the execution of the next handler, which might be a another chained middleware or the final execution function. After that call has finished execution we log the total execution time.

Let’s go ahead and add it to the router to see our execution time! ⏱

As seen, the only change in the code here is on line 20.

Enter fullscreen mode Exit fullscreen mode

With that line of code we are telling Negroni to use the Logger middleware that we created, the middleware are executed in the order they are inserted.

If we execute this code and call the endpoint we end up with this log:

The logger middleware is executing!
Endpoint handler called
Execution time: 25.146µs
Enter fullscreen mode Exit fullscreen mode

The first and third line is generated from the middleware, whilst the second line is generated in the function endpointHandler; the final handler function for the endpoint. We’ve successfully made a middleware! 🙌

Let’s play with the idea that we also had an Authentication middleware (AuthMW), the AuthMW would make sure that no unauthorised requests reaches the execution function. To add this to the chain we would simply do the following:

Enter fullscreen mode Exit fullscreen mode

With that change, we have built the first parts of a middleware chain. 👏

The code above would execute the logger before continuing to the Auth middleware. In the same fashion you could build a chain of middleware for individual router paths, if we combine these two methods of chaining middleware we end up with some pretty powerful tools for building our routes and the code we want them to execute!

The final code is available below.

Middleware example

Run the code:

go run main.go

Try calling the endpoint with

curl localhost:8080/ -v

I hope that you found this interesting and that you learned something!

If you did, you might find my previous article interesting as well. In it I go through how to build a REST API with a MySQL server connection in Go 🚀

Furthermore, If you liked this article sharing it with friends or on Twitter is greatly appreciated!

Liquid error: internal

Although this article took way longer than anticipated to write. It is my ambition to continue writing short guides like this, if you have any suggestions please comment below.

Happy coding! </⌨>️

Top comments (0)