DEV Community

Didik Tri Susanto
Didik Tri Susanto

Posted on • Originally published at blog.didiktrisusanto.dev on

Implement Your Own Request Middleware for Go HTTP Server

I believed this topic is a lot in the internet but I want to write it anyway, because documenting your own learning journey and share it to the world is good right?

Request middleware performs some specific function on the HTTP request or response at a specific stage in the HTTP pipeline before the business logic / app handler.

Currently middleware is a common term but in different programming language or framework sometimes it called filter. For illustration could be like this

http request middleware diagram

As you can see, middleware could be layered for more than one before hitting app handler or business logic then later it will return the response. HTTP Request comes in to first layer or middleware, check the necessary and if all conditions are passed then it allowed to continue the journey. If failed, it could also returned the failed response anyway.

Basic Middleware

Let say we have simple HTTP API server

func rootHandler() http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        resp := defaultResponse{
            Status:      "OK",
            Description: "Success",
        }

        j, _ := json.Marshal(resp)
        w.Header().Add("Content-Type", "application/json")
        w.Write(j)
    })
}

func main() {
    mux := http.NewServeMux()

    mux.Handle("GET /", rootHandler())

    s := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

if we run the program go run main.go and visit http://localhost:8080 it will return 200 success with JSON response:

{
  "status": "OK",
  "description": "Success"
}
Enter fullscreen mode Exit fullscreen mode

Now lets create our first middleware. Our middleware is to write a simple log that indicating the request is passing through. The middleware should be a function like this

func logMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Print("Executing logMiddleware")
        next.ServeHTTP(w, r)
    })
}
Enter fullscreen mode Exit fullscreen mode

Then we will modify the mux handler to use the middleware

func main() {
    mux := http.NewServeMux()
    mux.Handle("/", logMiddleware(rootHandler()))
    // ...
}
Enter fullscreen mode Exit fullscreen mode

The result is when we’re visiting the API, it also wrote the log that already specified in the middleware logic.

screenshot of running program with log middleware

Adding More Middleware

Interesting. Now try to add more middleware. This time we will check if the API request header contains x-signature, if not exists then we will return error.

Lets create the middleware function

func requestSignatureMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Print("Executing requestSignatureMiddleware")
        h := r.Header.Get("x-signature")
        if h == "" {
            resp := errorResponse{
                Status:  http.StatusUnauthorized,
                Error:   "INVALID_MISSING_SIGNATURE",
                Message: "missing request signature",
            }

            j, _ := json.Marshal(resp)
            w.Header().Add("Content-Type", "application/json")
            w.WriteHeader(http.StatusUnauthorized)
            w.Write(j)
            return
        }
        next.ServeHTTP(w, r)
    })
}
Enter fullscreen mode Exit fullscreen mode

We want to call logMiddleware first then continue to requestSignatureMiddleware. Our mux handler would be look like this now

func main() {
    mux := http.NewServeMux()
    mux.Handle("/", logMiddleware(requestSignatureMiddleware(rootHandler())))
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Here’s the result of the combined middlewares

screenshot of running multiple middlewares

  • First request without x-signature header will returned 401 HTTP status

  • Second request with additional x-signature header passed middleware logic and returned 200 HTTP status.

Cleaning Up

You may notice that our mux handler with multiple middlewares is little bit messy because it is calling function into function. What if we have many middlewares?

logMiddleware(requestSignatureMiddleware(authMiddleware(rateLimitMiddleware(rootHandler)))))

Hard to see.

Here’s what we can do, create a chain function to loop all middleware functions. You can named the function whatever you want.

// Middleware type for cleaner middleware chaining
type Middleware func(http.Handler) http.Handler

// Chain creates a single middleware from multiple middlewares
func Chain(middlewares ...Middleware) Middleware {
    return func(next http.Handler) http.Handler {
        for i := len(middlewares) - 1; i >= 0; i-- {
            next = middlewares[i](next)
        }
        return next
    }
}
Enter fullscreen mode Exit fullscreen mode

Initiate the Chain with our middlewares and finally call it in our mux handler.

func main() {
    mux := http.NewServeMux()

    // Create a chain of middlewares
    middlewareChain := Chain(
        func(next http.Handler) http.Handler { return logMiddleware(next) },
        func(next http.Handler) http.Handler { return requestSignatureMiddleware(next) },
    // Add more middlewares here as needed
    )

    mux.Handle("GET /", middlewareChain(rootHandler()))
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Now it’s more manageable and it will still got same result as before.

So here’s the full code of this experiment https://gist.github.com/didikz/98de868f20887285f5aabc80f4dddcfa

Happy coding!

Tiger Data image

🐯 🚀 Timescale is now TigerData

Building the modern PostgreSQL for the analytical and agentic era.

Read more

Top comments (0)

Tiger Data image

🐯 🚀 Timescale is now TigerData: Building the Modern PostgreSQL for the Analytical and Agentic Era

We’ve quietly evolved from a time-series database into the modern PostgreSQL for today’s and tomorrow’s computing, built for performance, scale, and the agentic future.

So we’re changing our name: from Timescale to TigerData. Not to change who we are, but to reflect who we’ve become. TigerData is bold, fast, and built to power the next era of software.

Read more

👋 Kindness is contagious

Explore this compelling article, highly praised by the collaborative DEV Community. All developers, whether just starting out or already experienced, are invited to share insights and grow our collective expertise.

A quick “thank you” can lift someone’s spirits—drop your kudos in the comments!

On DEV, sharing experiences sparks innovation and strengthens our connections. If this post resonated with you, a brief note of appreciation goes a long way.

Get Started