Skip to Content
Course content

52: Routing and Middleware Patterns

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

I've seen this happen in almost every mid-sized Go project I've audited: the "Handler Bloat." It starts innocently. You have a few API endpoints for a project management tool—maybe one to list tasks and one to create a new one. You realize you need to make sure the user is authenticated, so you add a few lines of auth logic at the top of your handler. Then you decide you want to log the request duration. You add that too. For two endpoints, it's a minor annoyance. For twenty? It's a maintenance nightmare.

The Copy-Paste Trap

When you're first starting with net/http, the instinct is to treat the handler as the one-stop shop for everything. You end up with something that looks like this:

func HandleCreateTask(w http.ResponseWriter, r *http.Request) {
    // Auth logic (repeated everywhere)
    user, err := authenticate(r)
    if err != nil {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }

    // Logging logic (repeated everywhere)
    start := time.Now()
    defer func() {
        log.Printf("CreateTask took %s", time.Since(start))
    }()

    // Finally, the actual business logic
    var task Task
    if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }
    saveTask(user.ID, task)
    w.WriteHeader(http.StatusCreated)
}

The problem here isn't just that you're typing the same thing over and over. The problem is that your actual business logic—saving a task—is buried under a mountain of plumbing. If you decide to change how authentication works, or if you want to switch from a simple log to a structured JSON logger, you have to touch every single handler in your application. That's a recipe for missing one and introducing a security hole.

The Handler Wrapper Pattern

The better way is to embrace the fact that http.Handler is just an interface. In Go, middleware isn't some magical framework feature; it's just a function that takes a handler and returns a new handler. I like to think of it as an onion. Each layer of middleware does its job and then decides whether to pass the request deeper into the core.

Here is how we decouple that plumbing from the logic:

func EnsureAuth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user, err := authenticate(r)
        if err != nil {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return // We stop the chain here
        }
        // We can even inject the user into the request context for the next handler
        ctx := context.WithValue(r.Context(), "user", user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func Logger(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r) // Call the next handler first
        log.Printf("%s %s took %s", r.Method, r.URL.Path, time.Since(start))
    })
}

Now, look at how clean your handler becomes. It only cares about tasks. It assumes that if it's being executed, the user is already authenticated because the middleware handled that guard rail.

func HandleCreateTask(w http.ResponseWriter, r *http.Request) {
    user := r.Context().Value("user").(*User)
    var task Task
    if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }
    saveTask(user.ID, task)
    w.WriteHeader(http.StatusCreated)
}

Connecting the Chain

The last piece of the puzzle is how you actually apply these. If you're using the standard library's ServeMux, you wrap your handlers during registration. It looks a bit like a Russian nesting doll:

mux := http.NewServeMux()
taskHandler := http.HandlerFunc(HandleCreateTask)

// Wrap the handler in auth, then wrap the result in logging
protectedChain := Logger(EnsureAuth(taskHandler))

mux.Handle("/tasks/create", protectedChain)

I'll be honest: nesting functions like Logger(EnsureAuth(Handler)) gets ugly fast when you have five or six layers. In a professional setting, you'll often see a "Constructor" pattern or a third-party library like chi or negroni that lets you define a stack of middleware. But regardless of the library, the underlying principle remains the same: http.Handler in, http.Handler out. This separation of concerns means your business logic stays pure, and your infrastructure logic stays centralized.




📋 Practical Task

Build a Rate-Limiting Middleware for a Project API

You are building a public API for a task manager, and you've noticed a few users are spamming the /tasks/search endpoint, slowing down the database for everyone else. Your goal is to implement a simple rate-limiting middleware that prevents any single IP address from making more than 5 requests per minute.

Requirements:

  • Create a middleware function called LimitRequests that takes an http.Handler and returns an http.Handler.
  • Use a map[string]int (protected by a sync.Mutex) to track the number of requests per IP address.
  • If a user exceeds 5 requests, return a 429 Too Many Requests status code and stop the chain.
  • If they are under the limit, increment their count and call next.ServeHTTP().
  • Apply this middleware only to the /tasks/search route, while leaving the /tasks/view route unrestricted.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.