DEV Community

Jones Charles
Jones Charles

Posted on

Go HTTP Middleware: Build Better APIs with These Patterns

Why Middleware Matters

Think of your Go API as a busy kitchen. HTTP requests are orders coming in, and your handlers are the chefs cooking up responses. Middleware? They’re the prep stations—chopping veggies (logging), checking ingredients (authentication), or pacing orders (rate limiting). For Go developers with a year or two of experience, mastering middleware is your ticket to cleaner, scalable code.

Go’s net/http package is lightweight yet powerful, and middleware supercharges it with reusable components. In this guide, we’ll explore middleware patterns, share runnable code, and learn from real-world projects to help you build robust APIs.

What You’ll Learn:

  • What middleware is and why it’s a game-changer.
  • Four essential design patterns with practical examples.
  • Real-world applications (logging, auth, and more).
  • A mini-project to tie it all together.

Let’s roll up our sleeves and dive into Go middleware!

What is Go HTTP Middleware?

Middleware sits between an HTTP request and your core logic, handling tasks like logging or authentication. In Go, it’s built on the http.Handler interface, letting you wrap handlers to add functionality without messing with the main code.

Why It’s Awesome

  • Simple: Go’s functional style keeps middleware clean.
  • Fast: Paired with goroutines, it handles high traffic like a champ.
  • Modular: Chain middleware for flexible, reusable designs.
Feature Why It Rocks
Simplicity Clean, readable code
Performance Goroutines for high concurrency
Composability Chain multiple features easily

Real-World Win: In an e-commerce API I worked on, middleware separated logging and auth from business logic, making the codebase a breeze to maintain.


Middleware Design Patterns: Your Go Toolkit

Middleware in Go is like stacking LEGO blocks—each piece adds a feature, and you can combine them to build something awesome. Let’s explore four key patterns to level up your API game.

1. Basic Middleware: Start Simple

Want to log request details like method, path, and duration? The basic middleware pattern wraps a handler to add functionality without touching the core logic.

Try This: Log every request’s details to debug performance.

package main

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

// loggingMiddleware logs request method, path, and duration.
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s took %s", r.Method, r.URL.Path, time.Since(start))
    })
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Wraps an http.Handler to log before and after the request.
  • Uses http.HandlerFunc for simplicity.
  • Tracks duration with time.Now().

Real-World Tip: In an API I built, this caught slow endpoints. Watch Out: High traffic can flood logs. Use sampling or async logging (e.g., go.uber.org/zap) to keep it lean.

2. Chained Middleware: Stack ’Em Up

Need logging, auth, and rate limiting? Chain middleware to combine features in a modular way.

Try This: Combine multiple middleware for a secure, monitored API.

package main

import (
    "net/http"
)

// chainMiddlewares applies middleware in reverse order.
func chainMiddlewares(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Applies middleware in reverse order (outermost runs first).
  • Keeps your code DRY and extensible.

Order Matters:

Middleware Job Run Order
Logging Tracks request details First
Authentication Checks JWT Second
Rate Limiting Caps request frequency Third

Real-World Tip: In a user auth microservice, chaining ensured rate limiting blocked bots before auth checks. Watch Out: Putting auth after rate limiting can leak resources. Always secure first!

3. Context Enhancement: Pass Data Down

Need to share data like a user ID with handlers? Use context.Context to safely pass info downstream.

Try This: Validate a JWT and inject the user ID.

package main

import (
    "context"
    "net/http"
)

// authMiddleware validates JWT and adds userID to context.
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        userID := validateToken(token)
        if userID == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        ctx := context.WithValue(r.Context(), "userID", userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func validateToken(token string) string {
    if token == "valid-token" {
        return "12345"
    }
    return ""
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Checks the Authorization header for a token.
  • Stores the user ID in the context.
  • Passes the updated context to the next handler.

Real-World Tip: In a session API, this made user data accessible without cluttering handlers. Watch Out: Don’t stuff big objects in context—it bloats memory. Stick to small data like IDs.

4. Error Handling: Keep It Stable

Panics can crash your server. Error-handling middleware catches them and returns clean 500 responses.

Try This: Catch panics to avoid downtime.

package main

import (
    "log"
    "net/http"
)

// recoveryMiddleware catches panics and returns 500 errors.
func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
                log.Printf("Panic: %v", err)
            }
        }()
        next.ServeHTTP(w, r)
    })
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Uses defer and recover() to catch panics.
  • Logs errors and sends a standard 500 response.

Real-World Tip: In a payment API, this saved us from third-party library crashes. Watch Out: Don’t just catch and ignore—log and fix the root cause.

What’s Your Favorite Pattern? Drop a comment below and share how you use middleware in your Go projects!


Real-World Middleware: Solve Common API Problems

Middleware is your API’s Swiss Army knife, tackling logging, authentication, rate limiting, and more. Let’s dive into four practical use cases with code and lessons learned.

1. Logging: Track Everything

Want to debug or monitor your API? Logging middleware captures request details like method, path, status, and duration.

Try This: Log requests in JSON for easy analysis.

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"
)

// responseWriterWrapper captures status code.
type responseWriterWrapper struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriterWrapper) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// loggingMiddleware logs request details in JSON.
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rw := &responseWriterWrapper{ResponseWriter: w, statusCode: http.StatusOK}
        next.ServeHTTP(rw, r)
        logEntry := map[string]interface{}{
            "method":   r.Method,
            "path":     r.URL.Path,
            "duration": time.Since(start).String(),
            "status":   rw.statusCode,
        }
        logData, _ := json.Marshal(logEntry)
        log.Println(string(logData))
    })
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Wraps http.ResponseWriter to capture status codes.
  • Logs details in JSON for tools like ELK or Grafana.

Real-World Tip: In an e-commerce API, this pinpointed slow endpoints. Watch Out: JSON logging can be heavy. Use async logging (e.g., go.uber.org/zap) to boost performance.

2. Authentication: Lock It Down

Secure your API by validating JWTs and passing user data to handlers.

Try This: Check tokens and inject user IDs into the context.

package main

import (
    "context"
    "net/http"
)

// authMiddleware validates JWT and adds userID to context.
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        userID := validateToken(token)
        if userID == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        ctx := context.WithValue(r.Context(), "userID", userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func validateToken(token string) string {
    if token == "Bearer valid-token" {
        return "12345"
    }
    return ""
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Grabs the Authorization header and validates the token.
  • Stores the user ID in the context for downstream use.

Real-World Tip: In a user management API, this kept auth logic clean. Watch Out: Mixing up 401 (Unauthorized) and 500 errors confuses clients. Always return 401 for invalid tokens.

3. Rate Limiting: Keep It Stable

Protect your API from abuse by limiting request frequency.

Try This: Use a token bucket to cap requests at 10 per second.

package main

import (
    "net/http"
    "golang.org/x/time/rate"
    "sync"
)

// rateLimitMiddleware limits requests per second.
func rateLimitMiddleware(next http.Handler, rps int) http.Handler {
    limiter := rate.NewLimiter(rate.Limit(rps), rps)
    var mu sync.Mutex
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        mu.Lock()
        err := limiter.Wait(r.Context())
        mu.Unlock()
        if err != nil {
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Uses golang.org/x/time/rate for token bucket rate limiting.
  • Rejects excess requests with a 429 status.

Real-World Tip: In a promotional API, this handled traffic spikes. Watch Out: Too-tight limits block legit users. Use Redis for distributed limiting or Prometheus for dynamic thresholds.

4. CORS: Play Nice with Frontends

Enable cross-origin requests for your front-end apps.

Try This: Add CORS headers for specific domains.

package main

import (
    "net/http"
)

// corsMiddleware enables CORS for specific origins.
func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "https://example.com")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
        w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusOK)
            return
        }
        next.ServeHTTP(w, r)
    })
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Sets CORS headers for allowed origins and methods.
  • Handles OPTIONS preflight requests.

Real-World Tip: In a front-end/back-end project, this ensured smooth cross-origin calls. Watch Out: Using * for Allow-Origin is a security risk. Always specify domains.

Got a Middleware Use Case? Share how you’ve used middleware in your APIs in the comments!


Best Practices: Write Middleware Like a Pro

Building middleware is like cooking a great dish—use the right ingredients and avoid common mistakes. Here’s how to make your Go middleware shine.

Top Tips

  1. Keep It Modular: Write single-purpose middleware for reusability and easy testing.
  2. Stay Fast: Avoid blocking ops; use async logging or caching for speed.
  3. Standardize Errors: Return consistent JSON error responses for clients.
  4. Test Everything: Use httptest to cover both happy paths and errors.

Common Gotchas

  • Wrong Order: Put auth before rate limiting to secure your API first. Fix: Plan your middleware chain carefully.
  • Resource Leaks: Forgetting defer r.Body.Close() can hog resources. Fix: Always close request bodies.
  • Goroutine Leaks: Unmanaged goroutines cause memory issues. Fix: Use context to control lifecycles.

Build It: A User Management API

Let’s tie it all together with a simple API that uses logging, auth, and rate-limiting middleware. This mini-project shows how middleware makes your code clean and robust.

Try This: Run a user info API with middleware in action.

package main

import (
    "context"
    "log"
    "net/http"
    "time"
    "golang.org/x/time/rate"
)

// responseWriterWrapper captures status code.
type responseWriterWrapper struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriterWrapper) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// loggingMiddleware logs request details.
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rw := &responseWriterWrapper{ResponseWriter: w, statusCode: http.StatusOK}
        next.ServeHTTP(rw, r)
        log.Printf("Method: %s, Path: %s, Status: %d, Duration: %s",
            r.Method, r.URL.Path, rw.statusCode, time.Since(start))
    })
}

// authMiddleware validates JWT and adds userID.
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token != "Bearer valid-token" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        ctx := context.WithValue(r.Context(), "userID", "12345")
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// rateLimitMiddleware caps at 10 requests/second.
func rateLimitMiddleware(next http.Handler) http.Handler {
    limiter := rate.NewLimiter(rate.Limit(10), 10)
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if err := limiter.Wait(r.Context()); err != nil {
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// chainMiddlewares applies middleware in order.
func chainMiddlewares(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
        userID := r.Context().Value("userID").(string)
        w.Write([]byte("User ID: " + userID))
    })

    handler := chainMiddlewares(mux, loggingMiddleware, rateLimitMiddleware, authMiddleware)
    log.Fatal(http.ListenAndServe(":8080", handler))
}
Enter fullscreen mode Exit fullscreen mode

How to Run:

  1. Save as main.go and run: go run main.go.
  2. Test with: curl -H "Authorization: Bearer valid-token" http://localhost:8080/users.
    • Success: User ID: 12345.
    • Errors: 401 for bad tokens, 429 for too many requests.

Real-World Tip: In production, use a real JWT library (e.g., github.com/golang-jwt/jwt) and Redis for rate limiting.


Wrapping Up

Key Takeaways

  • Middleware Rocks: Use http.Handler for flexible, high-performance APIs.
  • Patterns Galore: Basic, chained, context, and error-handling patterns cover most needs.
  • Stay Smart: Modular design and proper error handling keep your API robust.
  • Avoid Pitfalls: Watch for order errors, resource leaks, and goroutine issues.

Level Up Your Skills

  • Try Libraries: Check out gorilla/mux or go-chi/chi for advanced routing.
  • Test Performance: Use wrk to benchmark your API under load.
  • Join the Community: Follow Go’s blog (https://go.dev/blog) or Dev.to’s Go tag (https://dev.to/t/go).

Final Thought: Middleware saved my e-commerce API from traffic spikes—modularity was the secret sauce!

What’s Your Next Middleware Project? Share your ideas or questions in the comments!

Go HTTP Middleware: Build Better APIs with These Patterns

Why Middleware Matters

Think of your Go API as a busy kitchen. HTTP requests are orders coming in, and your handlers are the chefs cooking up responses. Middleware? They’re the prep stations—chopping veggies (logging), checking ingredients (authentication), or pacing orders (rate limiting). For Go developers with a year or two of experience, mastering middleware is your ticket to cleaner, scalable code.

Go’s net/http package is lightweight yet powerful, and middleware supercharges it with reusable components. In this guide, we’ll explore middleware patterns, share runnable code, and learn from real-world projects to help you build robust APIs.

What You’ll Learn:

  • What middleware is and why it’s a game-changer.
  • Four essential design patterns with practical examples.
  • Real-world applications (logging, auth, and more).
  • A mini-project to tie it all together.

Let’s roll up our sleeves and dive into Go middleware!

What is Go HTTP Middleware?

Middleware sits between an HTTP request and your core logic, handling tasks like logging or authentication. In Go, it’s built on the http.Handler interface, letting you wrap handlers to add functionality without messing with the main code.

Why It’s Awesome

  • Simple: Go’s functional style keeps middleware clean.
  • Fast: Paired with goroutines, it handles high traffic like a champ.
  • Modular: Chain middleware for flexible, reusable designs.
Feature Why It Rocks
Simplicity Clean, readable code
Performance Goroutines for high concurrency
Composability Chain multiple features easily

Real-World Win: In an e-commerce API I worked on, middleware separated logging and auth from business logic, making the codebase a breeze to maintain.


Middleware Design Patterns: Your Go Toolkit

Middleware in Go is like stacking LEGO blocks—each piece adds a feature, and you can combine them to build something awesome. Let’s explore four key patterns to level up your API game.

1. Basic Middleware: Start Simple

Want to log request details like method, path, and duration? The basic middleware pattern wraps a handler to add functionality without touching the core logic.

Try This: Log every request’s details to debug performance.

package main

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

// loggingMiddleware logs request method, path, and duration.
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s took %s", r.Method, r.URL.Path, time.Since(start))
    })
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Wraps an http.Handler to log before and after the request.
  • Uses http.HandlerFunc for simplicity.
  • Tracks duration with time.Now().

Real-World Tip: In an API I built, this caught slow endpoints. Watch Out: High traffic can flood logs. Use sampling or async logging (e.g., go.uber.org/zap) to keep it lean.

2. Chained Middleware: Stack ’Em Up

Need logging, auth, and rate limiting? Chain middleware to combine features in a modular way.

Try This: Combine multiple middleware for a secure, monitored API.

package main

import (
    "net/http"
)

// chainMiddlewares applies middleware in reverse order.
func chainMiddlewares(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Applies middleware in reverse order (outermost runs first).
  • Keeps your code DRY and extensible.

Order Matters:

Middleware Job Run Order
Logging Tracks request details First
Authentication Checks JWT Second
Rate Limiting Caps request frequency Third

Real-World Tip: In a user auth microservice, chaining ensured rate limiting blocked bots before auth checks. Watch Out: Putting auth after rate limiting can leak resources. Always secure first!

3. Context Enhancement: Pass Data Down

Need to share data like a user ID with handlers? Use context.Context to safely pass info downstream.

Try This: Validate a JWT and inject the user ID.

package main

import (
    "context"
    "net/http"
)

// authMiddleware validates JWT and adds userID to context.
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        userID := validateToken(token)
        if userID == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        ctx := context.WithValue(r.Context(), "userID", userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func validateToken(token string) string {
    if token == "valid-token" {
        return "12345"
    }
    return ""
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Checks the Authorization header for a token.
  • Stores the user ID in the context.
  • Passes the updated context to the next handler.

Real-World Tip: In a session API, this made user data accessible without cluttering handlers. Watch Out: Don’t stuff big objects in context—it bloats memory. Stick to small data like IDs.

4. Error Handling: Keep It Stable

Panics can crash your server. Error-handling middleware catches them and returns clean 500 responses.

Try This: Catch panics to avoid downtime.

package main

import (
    "log"
    "net/http"
)

// recoveryMiddleware catches panics and returns 500 errors.
func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
                log.Printf("Panic: %v", err)
            }
        }()
        next.ServeHTTP(w, r)
    })
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Uses defer and recover() to catch panics.
  • Logs errors and sends a standard 500 response.

Real-World Tip: In a payment API, this saved us from third-party library crashes. Watch Out: Don’t just catch and ignore—log and fix the root cause.

What’s Your Favorite Pattern? Drop a comment below and share how you use middleware in your Go projects!


Real-World Middleware: Solve Common API Problems

Middleware is your API’s Swiss Army knife, tackling logging, authentication, rate limiting, and more. Let’s dive into four practical use cases with code and lessons learned.

1. Logging: Track Everything

Want to debug or monitor your API? Logging middleware captures request details like method, path, status, and duration.

Try This: Log requests in JSON for easy analysis.

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"
)

// responseWriterWrapper captures status code.
type responseWriterWrapper struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriterWrapper) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// loggingMiddleware logs request details in JSON.
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rw := &responseWriterWrapper{ResponseWriter: w, statusCode: http.StatusOK}
        next.ServeHTTP(rw, r)
        logEntry := map[string]interface{}{
            "method":   r.Method,
            "path":     r.URL.Path,
            "duration": time.Since(start).String(),
            "status":   rw.statusCode,
        }
        logData, _ := json.Marshal(logEntry)
        log.Println(string(logData))
    })
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Wraps http.ResponseWriter to capture status codes.
  • Logs details in JSON for tools like ELK or Grafana.

Real-World Tip: In an e-commerce API, this pinpointed slow endpoints. Watch Out: JSON logging can be heavy. Use async logging (e.g., go.uber.org/zap) to boost performance.

2. Authentication: Lock It Down

Secure your API by validating JWTs and passing user data to handlers.

Try This: Check tokens and inject user IDs into the context.

package main

import (
    "context"
    "net/http"
)

// authMiddleware validates JWT and adds userID to context.
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        userID := validateToken(token)
        if userID == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        ctx := context.WithValue(r.Context(), "userID", userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func validateToken(token string) string {
    if token == "Bearer valid-token" {
        return "12345"
    }
    return ""
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Grabs the Authorization header and validates the token.
  • Stores the user ID in the context for downstream use.

Real-World Tip: In a user management API, this kept auth logic clean. Watch Out: Mixing up 401 (Unauthorized) and 500 errors confuses clients. Always return 401 for invalid tokens.

3. Rate Limiting: Keep It Stable

Protect your API from abuse by limiting request frequency.

Try This: Use a token bucket to cap requests at 10 per second.

package main

import (
    "net/http"
    "golang.org/x/time/rate"
    "sync"
)

// rateLimitMiddleware limits requests per second.
func rateLimitMiddleware(next http.Handler, rps int) http.Handler {
    limiter := rate.NewLimiter(rate.Limit(rps), rps)
    var mu sync.Mutex
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        mu.Lock()
        err := limiter.Wait(r.Context())
        mu.Unlock()
        if err != nil {
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Uses golang.org/x/time/rate for token bucket rate limiting.
  • Rejects excess requests with a 429 status.

Real-World Tip: In a promotional API, this handled traffic spikes. Watch Out: Too-tight limits block legit users. Use Redis for distributed limiting or Prometheus for dynamic thresholds.

4. CORS: Play Nice with Frontends

Enable cross-origin requests for your front-end apps.

Try This: Add CORS headers for specific domains.

package main

import (
    "net/http"
)

// corsMiddleware enables CORS for specific origins.
func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "https://example.com")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
        w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusOK)
            return
        }
        next.ServeHTTP(w, r)
    })
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Sets CORS headers for allowed origins and methods.
  • Handles OPTIONS preflight requests.

Real-World Tip: In a front-end/back-end project, this ensured smooth cross-origin calls. Watch Out: Using * for Allow-Origin is a security risk. Always specify domains.

Got a Middleware Use Case? Share how you’ve used middleware in your APIs in the comments!


Best Practices: Write Middleware Like a Pro

Building middleware is like cooking a great dish—use the right ingredients and avoid common mistakes. Here’s how to make your Go middleware shine.

Top Tips

  1. Keep It Modular: Write single-purpose middleware for reusability and easy testing.
  2. Stay Fast: Avoid blocking ops; use async logging or caching for speed.
  3. Standardize Errors: Return consistent JSON error responses for clients.
  4. Test Everything: Use httptest to cover both happy paths and errors.

Common Gotchas

  • Wrong Order: Put auth before rate limiting to secure your API first. Fix: Plan your middleware chain carefully.
  • Resource Leaks: Forgetting defer r.Body.Close() can hog resources. Fix: Always close request bodies.
  • Goroutine Leaks: Unmanaged goroutines cause memory issues. Fix: Use context to control lifecycles.

Build It: A User Management API

Let’s tie it all together with a simple API that uses logging, auth, and rate-limiting middleware. This mini-project shows how middleware makes your code clean and robust.

Try This: Run a user info API with middleware in action.

package main

import (
    "context"
    "log"
    "net/http"
    "time"
    "golang.org/x/time/rate"
)

// responseWriterWrapper captures status code.
type responseWriterWrapper struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriterWrapper) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// loggingMiddleware logs request details.
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rw := &responseWriterWrapper{ResponseWriter: w, statusCode: http.StatusOK}
        next.ServeHTTP(rw, r)
        log.Printf("Method: %s, Path: %s, Status: %d, Duration: %s",
            r.Method, r.URL.Path, rw.statusCode, time.Since(start))
    })
}

// authMiddleware validates JWT and adds userID.
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token != "Bearer valid-token" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        ctx := context.WithValue(r.Context(), "userID", "12345")
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// rateLimitMiddleware caps at 10 requests/second.
func rateLimitMiddleware(next http.Handler) http.Handler {
    limiter := rate.NewLimiter(rate.Limit(10), 10)
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if err := limiter.Wait(r.Context()); err != nil {
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// chainMiddlewares applies middleware in order.
func chainMiddlewares(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
        userID := r.Context().Value("userID").(string)
        w.Write([]byte("User ID: " + userID))
    })

    handler := chainMiddlewares(mux, loggingMiddleware, rateLimitMiddleware, authMiddleware)
    log.Fatal(http.ListenAndServe(":8080", handler))
}
Enter fullscreen mode Exit fullscreen mode

How to Run:

  1. Save as main.go and run: go run main.go.
  2. Test with: curl -H "Authorization: Bearer valid-token" http://localhost:8080/users.
    • Success: User ID: 12345.
    • Errors: 401 for bad tokens, 429 for too many requests.

Real-World Tip: In production, use a real JWT library (e.g., github.com/golang-jwt/jwt) and Redis for rate limiting.


Wrapping Up

Key Takeaways

  • Middleware Rocks: Use http.Handler for flexible, high-performance APIs.
  • Patterns Galore: Basic, chained, context, and error-handling patterns cover most needs.
  • Stay Smart: Modular design and proper error handling keep your API robust.
  • Avoid Pitfalls: Watch for order errors, resource leaks, and goroutine issues.

Level Up Your Skills

  • Try Libraries: Check out gorilla/mux or go-chi/chi for advanced routing.
  • Test Performance: Use wrk to benchmark your API under load.
  • Join the Community: Follow Go’s blog (https://go.dev/blog) or Dev.to’s Go tag (https://dev.to/t/go).

Final Thought: Middleware saved my e-commerce API from traffic spikes—modularity was the secret sauce!

What’s Your Next Middleware Project? Share your ideas or questions in the comments!

Top comments (0)