Making and using HTTP Middleware in Go
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.
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 thenext
handler, and then logs another message.We've made two normal handler functions,
fooHandler
andbarHandler
, which both log a message and send a200 OK
response.In the route
mux.Handle("GET /foo", http.HandlerFunc(fooHandler))
, we use thehttp.HandlerFunc()
function to convertfooHandler
to 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 convertbarHandler
to ahttp.Handler
, and then pass it to themiddlewareOne
function as thenext
argument. Or in simpler terms — we wrapbarHandler
with themiddlewareOne
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:
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
serverHeader
middleware that adds theServer: Go
header to HTTP responses. - A
logRequest
middleware that uses thelog/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.
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.