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!
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, please take a moment to consider the structure of the messageHandler function in the following code:
func main() {
mux := http.NewServeMux()
mux.Handle("GET /", messageHandler("Hello world!"))
log.Print("listening on :3000...")
err := http.ListenAndServe(":3000", mux)
log.Fatal(err)
}
func messageHandler(message string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(message))
})
}
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.
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 thenexthandler, and then logs another message.We've made two normal handler functions,
fooHandlerandbarHandler, which both log a message and send a200 OKresponse.In the route
mux.Handle("GET /foo", http.HandlerFunc(fooHandler)), we use thehttp.HandlerFunc()function to convertfooHandlerto ahttp.Handler, and use it as normal with no middleware.In the route
mux.Handle("GET /bar", middlewareOne(http.HandlerFunc(barHandler))), we use thehttp.HandlerFunc()function to convertbarHandlerto ahttp.Handler, and then pass it to themiddlewareOnefunction as thenextargument. Or in simpler terms — we wrapbarHandlerwith themiddlewareOnemiddleware 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:
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.
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
serverHeadermiddleware that adds theServer: Goheader to HTTP responses. - A
logRequestmiddleware that uses thelog/slogpackage 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.
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.
If you enjoyed this post...
You might like to check out my other Go tutorials on this site, or if you're after something more structured, my books Let's Go and Let's Go Further cover how to build complete, production-ready, web apps and APIS with Go.