Skip to Content
Course content

100: The Decorator Pattern via Middleware

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When I first started seeing people use frameworks like Gin or Echo, I noticed a recurring trend: developers treating "Middleware" as if it were a proprietary feature of the framework itself. I've seen countless juniors ask, "Which framework should I use to get the best middleware support?"

Here is the reality: Middleware isn't a framework feature. It's just a practical application of the Decorator Pattern using Go's first-class functions. If you think you need a library to handle "cross-cutting concerns" like logging or authentication, you're overcomplicating your stack.

Middleware isn't a Framework Feature; it's just a Function Signature

In Go, the net/http package gives us the http.Handler interface. Most of us spend our time writing functions that satisfy this interface. The "magic" of middleware happens when you realize that a middleware is simply a function that takes an http.Handler as an input and returns a new http.Handler as an output.

Look at this signature:

type Middleware func(http.Handler) http.Handler

That's it. That's the whole secret. The new handler returned by the middleware "wraps" the original one. It can do something before calling the original handler, something after, or even decide not to call the original handler at all (which is exactly how authentication guards work).

Stop Hardcoding Logic in Every Handler; Wrap the Behavior Instead

Imagine you're building a user profile API. You need to log every request for auditing and check if the request has a valid API key. The "naive" way is to paste that logic at the top of every single handler function. It's repetitive, it's brittle, and it's a nightmare to maintain. I've spent far too many hours scrubbing duplicate auth logic out of legacy codebases to recommend this approach.

Instead, we treat the core logic (fetching the user) as the "component" and the logging/auth as "decorators." Here is how that looks in practice:

package main

import (
	"fmt"
	"log"
	"net/http"
)

// This is our core logic. It doesn't know or care about logging or auth.
func userProfileHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "Welcome to your profile page!")
}

// LoggingMiddleware is a decorator. It adds logging behavior around any handler.
func LoggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Printf("Request received: %s %s", r.Method, r.URL.Path)
		next.ServeHTTP(w, r) // Call the original handler
		log.Printf("Request completed")
	})
}

// AuthMiddleware is another decorator. It can stop the chain entirely.
func AuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		apiKey := r.Header.Get("X-API-Key")
		if apiKey != "secret-token-123" {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return // We stop here; 'next' is never called.
		}
		next.ServeHTTP(w, r)
	})
}

func main() {
	// Wrap the core handler in multiple layers of decorators
	finalHandler := http.HandlerFunc(userProfileHandler)
	
	// Order matters! We log first, then check auth.
	wrappedHandler := LoggingMiddleware(AuthMiddleware(finalHandler))

	http.ListenAndServe(":8080", wrappedHandler)
}

I like to think of this as an onion. The core logic is at the center. Each middleware is a layer you wrap around it. When a request comes in, it hits the outermost layer first (Logging), moves inward (Auth), hits the center (User Profile), and then bounces back out through the layers.

One quick tip: as your chain grows, Middleware(Middleware(Middleware(handler))) gets ugly fast. In a real project, you'll likely write a small helper function to slice through a list of middlewares, but the underlying principle remains the same: you are decorating a function with more behavior.




📋 Practical Task

Building a Request-Timing and Panic-Recovery Pipeline

Your goal is to expand the middleware pattern to make your server more resilient and observable. You will build a pipeline that wraps a simple "Hello" handler with two specific decorators:

  • The Timer Middleware: It should capture the time when a request starts and, after the inner handler finishes, calculate the duration and print it to the console (e.g., "Request took 12ms").
  • The Recovery Middleware: This is critical for production. It should use defer and recover() to catch any panics that happen in the inner handlers. Instead of letting the whole server crash, it should recover and return a 500 Internal Server Error to the client.

Requirements:

  1. Create a handler that randomly panics (use rand.Intn) to test your recovery middleware.
  2. Ensure the Timer middleware correctly reports the duration even if the inner handler panics (Hint: the timer should be stopped in a defer block).
  3. Chain them so that the Recovery middleware is the outermost layer, ensuring it catches panics from both the Timer middleware and the final handler.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.