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))
})
}
How It Works:
- Wraps an
http.Handlerto log before and after the request. - Uses
http.HandlerFuncfor 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
}
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 ""
}
How It Works:
- Checks the
Authorizationheader 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)
})
}
How It Works:
- Uses
deferandrecover()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))
})
}
How It Works:
- Wraps
http.ResponseWriterto 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 ""
}
How It Works:
- Grabs the
Authorizationheader 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)
})
}
How It Works:
- Uses
golang.org/x/time/ratefor 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)
})
}
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
- Keep It Modular: Write single-purpose middleware for reusability and easy testing.
- Stay Fast: Avoid blocking ops; use async logging or caching for speed.
- Standardize Errors: Return consistent JSON error responses for clients.
-
Test Everything: Use
httptestto 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
contextto 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))
}
How to Run:
- Save as
main.goand run:go run main.go. - 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.
- Success:
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.Handlerfor 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/muxorgo-chi/chifor advanced routing. -
Test Performance: Use
wrkto 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))
})
}
How It Works:
- Wraps an
http.Handlerto log before and after the request. - Uses
http.HandlerFuncfor 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
}
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 ""
}
How It Works:
- Checks the
Authorizationheader 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)
})
}
How It Works:
- Uses
deferandrecover()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))
})
}
How It Works:
- Wraps
http.ResponseWriterto 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 ""
}
How It Works:
- Grabs the
Authorizationheader 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)
})
}
How It Works:
- Uses
golang.org/x/time/ratefor 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)
})
}
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
- Keep It Modular: Write single-purpose middleware for reusability and easy testing.
- Stay Fast: Avoid blocking ops; use async logging or caching for speed.
- Standardize Errors: Return consistent JSON error responses for clients.
-
Test Everything: Use
httptestto 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
contextto 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))
}
How to Run:
- Save as
main.goand run:go run main.go. - 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.
- Success:
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.Handlerfor 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/muxorgo-chi/chifor advanced routing. -
Test Performance: Use
wrkto 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)