AlgoMaster Logo

HTTP Middleware

High Priority13 min readUpdated June 6, 2026

Middleware in Go is a function that wraps an http.Handler to add behavior before, after, or around the wrapped handler's work. It's how you add logging, authentication, panic recovery, and timing without copying the same code into every handler. This lesson covers the wrapper pattern, three useful middlewares for an e-commerce API, chaining, and ordering rules.

The Wrapper Pattern

A middleware in Go has the signature func(http.Handler) http.Handler. It takes a handler, returns a new handler that does some work, then calls the original. The wrapped handler doesn't know it's been wrapped, which is the point: the cross-cutting concern stays out of the business code.

Here's the shape, with no real work yet:

next is the handler being wrapped. The returned http.HandlerFunc is itself an http.Handler. Calling next.ServeHTTP(w, r) runs the wrapped handler. Code above that line runs on the way in, code below runs on the way out.

A handler that lists products in a shop catalog might look like this:

passthrough adds no behavior yet, but the structure is in place. Every middleware in the rest of this lesson follows the same shape: take a handler, return a handler.

A Logger Middleware

Logging every request is the canonical first middleware. The fields most useful in production are the method, path, response status code, and duration. The first two are easy; the request carries them. Duration is a simple time.Now() at the start and time.Since at the end. The status code is the awkward one: by the time the handler returns, the response has already been written, and Go's http.ResponseWriter does not expose a way to read back the code.

The fix is a small wrapper around ResponseWriter that intercepts WriteHeader and remembers what was passed. Once you have that, the logger middleware is straightforward.

statusRecorder embeds http.ResponseWriter, so it inherits Write and Header() automatically. Overriding WriteHeader is enough to capture the status. The default status is 200 because a handler that writes a body without calling WriteHeader produces a 200 OK on the wire, and the recorder should mirror that.

A small server that uses it:

A Recover Middleware

A panic in a handler crashes the goroutine that handles the request. Go's HTTP server catches the panic itself and aborts the connection, but the panic message lands in stderr without context, and the client sees a connection drop instead of a proper 500. A recover middleware turns panics into clean 500 responses and logs the stack trace.

recover only works inside a deferred function, which is why the panic handling is wrapped in defer func(). If the wrapped handler panics, control jumps to the deferred function, recover() returns the value passed to panic, and the middleware writes a 500. If the handler returns normally, recover() returns nil and the deferred function does nothing.

A buggy handler to exercise it:

Recover middleware should sit close to the outer edge of the chain so it catches panics from every layer beneath it, including other middlewares. The only thing typically placed outside it is the logger, so failed requests still get logged.

An Auth Middleware

The third common middleware checks credentials before letting the request through. A real auth system involves tokens, sessions, and key rotation, but the shape of the middleware is the same regardless: read something off the request, validate it, either continue or reject.

For an internal e-commerce admin API, a simple header check is enough to illustrate the pattern. The middleware reads X-API-Key, compares it to an expected value, and rejects with 401 if it's missing or wrong.

apiKey is a middleware factory rather than a middleware itself. It takes the expected key as a parameter and returns a middleware bound to that key. That's a common pattern when a middleware needs configuration. The function value apiKey("secret123") has the right func(http.Handler) http.Handler signature to plug into any chain.

subtle.ConstantTimeCompare is the correct function for comparing secrets. A plain == on strings short-circuits at the first differing byte, which leaks key length and a small amount of content information through timing. For a low-value internal API the timing leak isn't a real risk, but using the constant-time function is one extra import and avoids teaching the wrong habit.

An admin handler protected by the middleware:

When the middleware needs to share data with the handler, like a verified user ID, that's the job of context.WithValue. For now the middleware either lets the request through unchanged or short-circuits with an error.

Chaining Middleware

Each middleware wraps a handler. To stack three of them, you nest the calls:

That reads inside-out, which gets ugly fast. A small helper flattens it:

The loop walks the middlewares in reverse so that the first one listed ends up on the outside. With chain in place, the registration reads top-to-bottom in the order requests flow through:

Reading that out loud: every request gets logged, then panics are recovered, then the API key is checked, then the handler runs. That's usually the order you want.

A full working server tying the three middlewares together:

The logger sits outermost, so it logs the 401 even though apiKey rejected the request before deleteOrder ran.

Order Matters

The order of middlewares in the chain decides what runs first on the way in and last on the way out. The outermost middleware sees the request first and the response last. The innermost middleware sees the request last and the response first. A request is a ball that drops through each layer, hits the handler, then bounces back out through the same layers in reverse.

The same three middlewares in a different order behave differently:

Order (outer to inner)Behavior
logging, recovering, apiKeyLogs all requests, including 401s. Panics get logged and turned into 500s.
apiKey, logging, recovering401 responses are not logged. The logger never runs for unauthorized requests.
recovering, logging, apiKeyA panic in logging itself isn't caught. The recover sits inside the logger.

The usual default is logger outermost, recover next, then auth, then any business-specific middlewares. Logger outermost means every request gets logged regardless of outcome. Recover outside of auth means a panic inside the auth check still becomes a clean 500. Auth before the handler means the handler runs only for authorized requests.

Per-Route vs Global Middleware

Some middlewares apply to every route. Logging usually does, recover usually does. Others apply to only some routes. Auth typically protects admin endpoints but not public ones like /products.

Per-route is the version shown so far: wrap the specific handler when registering it. Wrap each route with whichever middlewares it needs.

That works, but repeating logging, recovering on every route is noise. The cleaner pattern is to register routes on a mux with their per-route middleware (here, just auth on the admin route), then wrap the entire mux with the global middlewares.

/products is public, so the only middlewares around it are the global logger and recover. /admin/orders is wrapped with apiKey at registration time, then the whole mux is wrapped with logger and recover. Every request hits the global pair first, then the mux routes to the handler, applying any per-route middleware on the way.

This split scales well. New public endpoints register without extra wrapping. New admin endpoints register with apiKey(...) around the handler. Logging and recover stay in one place.

Quiz

HTTP Middleware Quiz

10 quizzes