Not sure how to structure your Go web application?

My new book guides you through the start-to-finish build of a real world web application in Go — covering topics like how to structure your code, manage dependencies, create dynamic database-driven pages, and how to authenticate and authorize users securely.

Take a look!

Making and using HTTP Middleware in Go

Last updated:
Level: Beginner

When you're building a web application, there's probably some shared functionality that you want to run for many (or even all) HTTP requests. You might want to log every request, gzip every response, or check that a user is authenticated before sending them any content.

One way of organizing this shared functionality is to set it up as middleware — essentially a self-contained block of code that independently acts on a request, before or after your normal application handlers.

In this post I'll explain how to create and use your own middleware, how to chain multiple middlewares together, and finish up with some practical real-world examples and tips.

The standard pattern

Before we talk about middleware, take a moment to consider the structure of the messageHandler function in the following code:

func messageHandler(message string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte(message))
	})
}

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

	mux.Handle("GET /", messageHandler("Hello world!"))

	log.Print("listening on :3000...")
	err := http.ListenAndServe(":3000", mux)
	log.Fatal(err)
}

In this code we put our messageHandler logic — which is just a call to w.Write() — in an anonymous function which 'closes over' the message variable to form a closure. We then convert the closure to an http.Handler with the http.HandlerFunc() adapter, and then return it.

We can use this same general pattern to help us create a middleware function. Instead of passing a string into the closure (like above), you can pass another http.Handler as a parameter, and then transfer control to this handler by calling its ServeHTTP() method. Like so:

func exampleMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Your middleware logic goes here...
		next.ServeHTTP(w, r)
	})
}

Essentially, the exampleMiddleware function accepts a next handler as a parameter, and it returns a closure which is also a handler. When this closure is executed, any code in the closure will be run and then the next handler will be called.

Using middleware on specific routes

If any of that sounds confusing, don't worry! In practice you can copy and paste that code pattern if you need to, and beyond that, making and using middleware is actually fairly straightforward.

Let's start by looking at an example of how to use middleware on specific routes in your application.

main.go
package main

import (
	"log"
	"net/http"
)

func middlewareOne(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Println(r.URL.Path, "executing middlewareOne")
		next.ServeHTTP(w, r)
		log.Println(r.URL.Path, "executing middlewareOne again")
	})
}

func fooHandler(w http.ResponseWriter, r *http.Request) {
	log.Println(r.URL.Path, "executing fooHandler")
	w.Write([]byte("OK"))
}

func barHandler(w http.ResponseWriter, r *http.Request) {
	log.Println(r.URL.Path, "executing barHandler")
	w.Write([]byte("OK"))
}

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

	mux.Handle("GET /foo", http.HandlerFunc(fooHandler))
	mux.Handle("GET /bar", middlewareOne(http.HandlerFunc(barHandler)))

	log.Print("listening on :3000...")
	err := http.ListenAndServe(":3000", mux)
	log.Fatal(err)
}	

There is quite a lot going on in this code, so let's take a moment to unpack some of it:

  • We've created a middleware function called middlewareOne, which uses the standard pattern that we talked about above. The middleware logs a message, calls the next handler, and then logs another message.

  • We've made two normal handler functions, fooHandler and barHandler, which both log a message and send a 200 OK response.

  • In the route mux.Handle("GET /foo", http.HandlerFunc(fooHandler)), we use the http.HandlerFunc() function to convert fooHandler to a http.Handler, and use it as normal with no middleware.

  • In the route mux.Handle("GET /bar", middlewareOne(http.HandlerFunc(barHandler))), we use the http.HandlerFunc() function to convert barHandler to a http.Handler, and then pass it to the middlewareOne function as the next argument. Or in simpler terms — we wrap barHandler with the middlewareOne middleware function.

If you run this application and make a request to http://localhost:3000/foo, you should see some log output containing only the message from fooHandler:

$ go run main.go
2025/07/05 19:00:56 listening on :3000...
2025/07/05 19:01:09 /foo executing fooHandler

In contrast, if you make a request to http://localhost:3000/bar, you should also see the log messages from middlewareOne, demonstrating that the middleware is successfully being used on that route.

...
2025/07/05 19:02:43 /bar executing middlewareOne
2025/07/05 19:02:43 /bar executing barHandler
2025/07/05 19:02:43 /bar executing middlewareOne again

This log output also nicely illustrates the flow of control through the application code. We can see that any code in middlewareOne which comes before next.ServeHTTP(w, r) runs before barHandler is executed — and any code which comes after next.ServeHTTP(w, r) runs after barHandler has returned.

So the flow of control through the application for the GET /bar route looks like this:

http.ServeMux → middlewareOne → barHandler → middlewareOne → http.ServeMux

Using middleware on all routes

In the previous example, we used our middleware to wrap a specific handler in a specific route. But if you want your middleware to act on all routes, you can wrap http.ServeMux itself so that the flow of control looks like this:

middlewareOne → http.ServeMux → fooHandler/barHandler → http.ServeMux → middlewareOne

This works because Go's http.ServeMux implements the http.Handler interface — it has the necessary ServeHTTP() method. And as a result, we can directly pass an http.ServeMux into a middleware function as the next parameter.

Let's update our example code to do this:

main.go
package main

...

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

	// We don't use any middleware on the individual routes.
	mux.Handle("GET /foo", http.HandlerFunc(fooHandler))
	mux.Handle("GET /bar", http.HandlerFunc(fooHandler))

	log.Println("listening on :3000...")
	// Wrap the http.ServeMux with the middlewareOne function.
	err := http.ListenAndServe(":3000", middlewareOne(mux))
	log.Fatal(err)
}

And if you run the application and make the same requests to /foo and /bar again, you should see from the log output that middlewareOne is now being used on all routes.

$ go run main.go
2025/07/05 19:04:48 listening on :3000...
2025/07/05 19:04:54 /foo executing middlewareOne
2025/07/05 19:04:54 /foo executing fooHandler
2025/07/05 19:04:54 /foo executing middlewareOne again
2025/07/05 19:04:58 /bar executing middlewareOne
2025/07/05 19:04:58 /bar executing fooHandler
2025/07/05 19:04:58 /bar executing middlewareOne again

Chaining middleware

Because the standard middleware function pattern accepts a http.Handler as a parameter, and it returns a http.Handler, that makes it possible to easily create arbitrarily long chains of middleware. Put simply, one middleware function can wrap another middleware function.

To illustrate this, let's add some more middleware functions to our example and chain them together.

main.go
package main

import (
	"log"
	"net/http"
)

func middlewareOne(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Println(r.URL.Path, "executing middlewareOne")
		next.ServeHTTP(w, r)
		log.Println(r.URL.Path, "executing middlewareOne again")
	})
}

func middlewareTwo(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Println(r.URL.Path, "executing middlewareTwo")
		next.ServeHTTP(w, r)
		log.Println(r.URL.Path, "executing middlewareTwo again")
	})
}

func middlewareThree(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Println(r.URL.Path, "executing middlewareThree")
		next.ServeHTTP(w, r)
		log.Println(r.URL.Path, "executing middlewareThree again")
	})
}

func middlewareFour(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Println(r.URL.Path, "executing middlewareFour")
		next.ServeHTTP(w, r)
		log.Println(r.URL.Path, "executing middlewareFour again")
	})
}

func middlewareFive(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Println(r.URL.Path, "executing middlewareFive")
		next.ServeHTTP(w, r)
		log.Println(r.URL.Path, "executing middlewareFive again")
	})
}

func fooHandler(w http.ResponseWriter, r *http.Request) {
	log.Println(r.URL.Path, "executing fooHandler")
	w.Write([]byte("OK"))
}

func barHandler(w http.ResponseWriter, r *http.Request) {
	log.Println(r.URL.Path, "executing barHandler")
	w.Write([]byte("OK"))
}

func main() {
	mux := http.NewServeMux()
	
	// Apply middlewareThree and middlewareFour to GET /foo
	mux.Handle("GET /foo", middlewareThree(middlewareFour(http.HandlerFunc(fooHandler))))
	
	// Apply middlewareFour and middlewareFive to GET /bar
	mux.Handle("GET /bar", middlewareFour(middlewareFive(http.HandlerFunc(barHandler))))
	
	log.Println("listening on :3000...")
	// Apply middlewareOne and middlewareTwo to the entire http.ServeMux
	err := http.ListenAndServe(":3000", middlewareOne(middlewareTwo(mux)))
	log.Fatal(err)
}

In this code we are now wrapping the http.ServeMux with middlewares One and Two, on the GET /foo route we're using middlewares Three and Four, and on the GET /bar route we're using middlewares Four and Five.

Again, if you run the application and make the same requests to /foo and /bar you should now see log output that demonstrates the middleware functions being chained together and the flow of control through them. Like so:

2025/07/05 19:06:25 /foo executing middlewareOne
2025/07/05 19:06:25 /foo executing middlewareTwo
2025/07/05 19:06:25 /foo executing middlewareThree
2025/07/05 19:06:25 /foo executing middlewareFour
2025/07/05 19:06:25 /foo executing fooHandler
2025/07/05 19:06:25 /foo executing middlewareFour again
2025/07/05 19:06:25 /foo executing middlewareThree again
2025/07/05 19:06:25 /foo executing middlewareTwo again
2025/07/05 19:06:25 /foo executing middlewareOne again
2025/07/05 19:06:43 /bar executing middlewareOne
2025/07/05 19:06:43 /bar executing middlewareTwo
2025/07/05 19:06:43 /bar executing middlewareFour
2025/07/05 19:06:43 /bar executing middlewareFive
2025/07/05 19:06:43 /bar executing barHandler
2025/07/05 19:06:43 /bar executing middlewareFive again
2025/07/05 19:06:43 /bar executing middlewareFour again
2025/07/05 19:06:43 /bar executing middlewareTwo again
2025/07/05 19:06:43 /bar executing middlewareOne again

Early returns

One of the useful things about middleware is that you can use it as a 'guard' to prevent downstream middleware and handlers in the chain from being executed unless certain conditions are met. For example, you can use middleware to check if a user is authenticated, or that a request contains the correct Content-Type header, or that the client hasn't hit a rate-limiter ceiling before doing any further processing.

For example, you could create a middleware function to ensure that the request Content-Type header exactly matches application/json by returning early from the middleware, before calling next.ServeHTTP(w, r). Like this:

func requireJSON(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		contentType := r.Header.Get("Content-Type")

		// If the content type is not application/json, send an error message and
		// return from the middleware. By returning before next.ServeHTTP(w, r)
		// is called, it means that the next handler in the chain is never executed.
		if contentType != "application/json" {
			http.Error(w, "Content-Type header must be application/json", http.StatusUnsupportedMediaType)
			return
		}

		// Otherwise, if the content type is application/json, call the next handler
		// in the chain as normal.
		next.ServeHTTP(w, r)
	})
}

A more realistic example

Now that we've covered the theory, let's look at a more practical example to give you a taste for using middleware in a real application.

In this code, we'll create two middleware functions that we want to use on all routes:

  • A serverHeader middleware that adds the Server: Go header to HTTP responses.
  • A logRequest middleware that uses the log/slog package to log the details of the current request.

And we'll also create a GET /admin route that is guarded by a requireBasicAuthentication middleware function, which requires the client to authenticate via HTTP basic authentication. This is another example where we will use the 'early return' pattern that we just talked about.

main.go
package main

import (
	"log/slog"
	"net/http"
	"os"
)

func serverHeader(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Server", "Go")
		next.ServeHTTP(w, r)
	})
}

func logRequest(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var (
			ip     = r.RemoteAddr
			method = r.Method
			url    = r.URL.String()
			proto  = r.Proto
		)

		userAttrs := slog.Group("user", "ip", ip)
		requestAttrs := slog.Group("request", "method", method, "url", url, "proto", proto)

		slog.Info("request received", userAttrs, requestAttrs)
		next.ServeHTTP(w, r)
	})
}

func requireBasicAuthentication(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		validUsername := "admin"
		validPassword := "secret"

		username, password, ok := r.BasicAuth()
		if !ok || username != validUsername || password != validPassword {
			w.Header().Set("WWW-Authenticate", `Basic realm="protected"`)
			http.Error(w, "401 Unauthorized", http.StatusUnauthorized)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func home(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Welcome to the home page!"))
}

func admin(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Admin dashboard - you are authenticated!"))
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /{$}", home)
	// Use the requireBasicAuthentication middleware on the GET /admin route only.
	mux.Handle("GET /admin", requireBasicAuthentication(http.HandlerFunc(admin)))

	slog.Info("listening on :3000...")
	// Use the serverHeader and logRequest middleware on all routes.
	err := http.ListenAndServe(":3000", serverHeader(logRequest(mux)))
	if err != nil {
		slog.Error(err.Error())
		os.Exit(1)
	}
}

Go ahead and run this application, then open a second terminal window and use curl to make a request to GET /, and unauthenticated and authenticated requests to GET /admin. The responses should look similar to this:

$ curl -i localhost:3000
HTTP/1.1 200 OK
Server: Go
Date: Sat, 05 Jul 2025 12:19:24 GMT
Content-Length: 25
Content-Type: text/plain; charset=utf-8

Welcome to the home page!

$ curl -i localhost:3000/admin
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
Server: Go
Www-Authenticate: Basic realm="protected"
X-Content-Type-Options: nosniff
Date: Sat, 05 Jul 2025 12:19:32 GMT
Content-Length: 17

401 Unauthorized

$ curl -i -u admin:secret localhost:3000/admin
HTTP/1.1 200 OK
Server: Go
Date: Sat, 05 Jul 2025 12:26:53 GMT
Content-Length: 40
Content-Type: text/plain; charset=utf-8

Admin dashboard - you are authenticated!

We can see from these responses that our serverHeader middleware is setting the Server: Go header on all responses, and that the requireBasicAuthentication middleware is correctly protecting our GET /admin route.

And if you head back to your original terminal window, you should see the corresponding log entries courtesy of the logRequest middleware. Similar to this:

$ go run main.go 
2025/07/05 14:18:44 INFO listening on :3000...
2025/07/05 14:19:24 INFO request received user.ip=127.0.0.1:41966 request.method=GET request.url=/ request.proto=HTTP/1.1
2025/07/05 14:19:32 INFO request received user.ip=127.0.0.1:59244 request.method=GET request.url=/admin request.proto=HTTP/1.1
2025/07/05 14:26:53 INFO request received user.ip=127.0.0.1:57670 request.method=GET request.url=/admin request.proto=HTTP/1.1

Managing and organizing middleware

Lastly, a couple of tips. If you have an application with lots of routes and lots of middleware, you can potentially end up with very long route declarations and a lot of duplication in those declarations, which isn't ideal for easy-reading or maintainability.

One of the tools that I've used for a long time to help manage this is justinas/alice, which is a small package that makes it easy to create reusable chains of handlers. At it's most basic, it let's you rewrite code that looks like this:

mux.Handle("GET /foo", middlewareOne(middlewareTwo(middlewareThree(http.HandlerFunc(fooHandler)))))
mux.Handle("GET /bar", middlewareOne(middlewareTwo(middlewareThree(http.HandlerFunc(barHandler)))))

As this:

stdChain := alice.New(middlewareOne, middlewareTwo, middlewareThree)

mux.Handle("/foo", stdChain.Then(fooHandler))
mux.Handle("/bar", stdChain.Then(barHandler))

More recently, I've been rolling my own custom chain type instead of using justinas/alice, or wrapping http.ServeMux so that it supports 'groups' of routes which use specific middleware. If you're interested in this, I've written a more about it in the post "Organize your Go middleware without dependencies", and it's probably a good follow-on read from this post.