API Gateways and Reverse Proxies in Go
Introduction
API gateways and reverse proxies are critical components in microservices architecture, providing a single entry point for client requests. This guide covers building these components in Go.
API gateways handle routing, authentication, rate limiting, and request transformation, while reverse proxies distribute traffic across backend services.
Reverse Proxy Basics
Simple Reverse Proxy
package main
import (
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
)
// SimpleReverseProxy creates a simple reverse proxy
func SimpleReverseProxy(targetURL string) http.Handler {
target, err := url.Parse(targetURL)
if err != nil {
log.Fatal(err)
}
proxy := httputil.NewSingleHostReverseProxy(target)
return proxy
}
// StartSimpleReverseProxy starts a simple reverse proxy
func StartSimpleReverseProxy() {
// Proxy requests to backend service
proxy := SimpleReverseProxy("http://localhost:8081")
fmt.Println("Starting reverse proxy on :8080")
log.Fatal(http.ListenAndServe(":8080", proxy))
}
Good: Proper API Gateway Implementation
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"sync"
"time"
)
// Route represents a route configuration
type Route struct {
Path string
Target string
Methods []string
}
// APIGateway implements an API gateway
type APIGateway struct {
routes []Route
proxies map[string]*httputil.ReverseProxy
rateLimiter *RateLimiter
mu sync.RWMutex
}
// NewAPIGateway creates a new API gateway
func NewAPIGateway() *APIGateway {
return &APIGateway{
routes: []Route{},
proxies: make(map[string]*httputil.ReverseProxy),
rateLimiter: NewRateLimiter(100, time.Second),
}
}
// AddRoute adds a route to the gateway
func (ag *APIGateway) AddRoute(path, target string, methods []string) error {
targetURL, err := url.Parse(target)
if err != nil {
return fmt.Errorf("invalid target URL: %w", err)
}
ag.mu.Lock()
defer ag.mu.Unlock()
ag.routes = append(ag.routes, Route{
Path: path,
Target: target,
Methods: methods,
})
proxy := httputil.NewSingleHostReverseProxy(targetURL)
proxy.Director = ag.createDirector(targetURL)
ag.proxies[path] = proxy
return nil
}
// createDirector creates a director function for the proxy
func (ag *APIGateway) createDirector(target *url.URL) func(*http.Request) {
return func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = strings.TrimPrefix(req.URL.Path, "/api")
req.RequestURI = ""
// Add headers
req.Header.Add("X-Forwarded-For", req.RemoteAddr)
req.Header.Add("X-Forwarded-Proto", "http")
}
}
// ServeHTTP implements http.Handler
func (ag *APIGateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Rate limiting
if !ag.rateLimiter.Allow() {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
// Authentication
if !ag.authenticate(r) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Find matching route
ag.mu.RLock()
var matchedRoute *Route
var proxy *httputil.ReverseProxy
for _, route := range ag.routes {
if strings.HasPrefix(r.URL.Path, route.Path) {
matchedRoute = &route
proxy = ag.proxies[route.Path]
break
}
}
ag.mu.RUnlock()
if matchedRoute == nil {
http.Error(w, "Not found", http.StatusNotFound)
return
}
// Check method
methodAllowed := false
for _, method := range matchedRoute.Methods {
if method == r.Method {
methodAllowed = true
break
}
}
if !methodAllowed {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Log request
log.Printf("%s %s -> %s", r.Method, r.URL.Path, matchedRoute.Target)
// Proxy request
proxy.ServeHTTP(w, r)
}
// authenticate authenticates the request
func (ag *APIGateway) authenticate(r *http.Request) bool {
// Check for API key
apiKey := r.Header.Get("X-API-Key")
if apiKey == "" {
return false
}
// Validate API key (simplified)
return apiKey == "valid-key"
}
// RateLimiter implements token bucket rate limiting
type RateLimiter struct {
tokens float64
maxTokens float64
refillRate float64
lastRefill time.Time
mu sync.Mutex
}
// NewRateLimiter creates a new rate limiter
func NewRateLimiter(maxTokens float64, refillInterval time.Duration) *RateLimiter {
return &RateLimiter{
tokens: maxTokens,
maxTokens: maxTokens,
refillRate: maxTokens / refillInterval.Seconds(),
lastRefill: time.Now(),
}
}
// Allow checks if a request is allowed
func (rl *RateLimiter) Allow() bool {
rl.mu.Lock()
defer rl.mu.Unlock()
// Refill tokens
now := time.Now()
elapsed := now.Sub(rl.lastRefill).Seconds()
rl.tokens = min(rl.maxTokens, rl.tokens+elapsed*rl.refillRate)
rl.lastRefill = now
if rl.tokens >= 1 {
rl.tokens--
return true
}
return false
}
func min(a, b float64) float64 {
if a < b {
return a
}
return b
}
// Example usage
func ExampleAPIGateway() {
gateway := NewAPIGateway()
// Add routes
gateway.AddRoute("/api/users", "http://localhost:8081", []string{"GET", "POST"})
gateway.AddRoute("/api/orders", "http://localhost:8082", []string{"GET", "POST"})
gateway.AddRoute("/api/products", "http://localhost:8083", []string{"GET"})
fmt.Println("Starting API gateway on :8080")
log.Fatal(http.ListenAndServe(":8080", gateway))
}
Bad: Improper API Gateway Implementation
package main
import (
"net/http"
)
// BAD: No authentication
type BadAPIGateway struct{}
// BAD: No rate limiting
func (bg *BadAPIGateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// No authentication
// No rate limiting
// No request validation
// Direct proxy without checks
w.WriteHeader(http.StatusOK)
}
Problems:
- No authentication
- No rate limiting
- No request validation
- No error handling
Advanced Gateway Features
Request/Response Transformation
package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
)
// RequestTransformer transforms requests
type RequestTransformer struct {
transformers []func(*http.Request) error
}
// NewRequestTransformer creates a new transformer
func NewRequestTransformer() *RequestTransformer {
return &RequestTransformer{
transformers: []func(*http.Request) error{},
}
}
// AddTransformer adds a transformer
func (rt *RequestTransformer) AddTransformer(fn func(*http.Request) error) {
rt.transformers = append(rt.transformers, fn)
}
// Transform transforms a request
func (rt *RequestTransformer) Transform(r *http.Request) error {
for _, transformer := range rt.transformers {
if err := transformer(r); err != nil {
return err
}
}
return nil
}
// Example transformers
func AddAuthHeader(token string) func(*http.Request) error {
return func(r *http.Request) error {
r.Header.Add("Authorization", "Bearer "+token)
return nil
}
}
func AddRequestID(r *http.Request) error {
r.Header.Add("X-Request-ID", "req-123")
return nil
}
func TransformRequestBody(r *http.Request) error {
if r.Body == nil {
return nil
}
body, err := io.ReadAll(r.Body)
if err != nil {
return err
}
var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil {
return err
}
// Transform data
data["transformed"] = true
newBody, err := json.Marshal(data)
if err != nil {
return err
}
r.Body = io.NopCloser(bytes.NewReader(newBody))
r.ContentLength = int64(len(newBody))
return nil
}
Circuit Breaker
package main
import (
"fmt"
"sync"
"time"
)
// CircuitBreakerState represents circuit breaker state
type CircuitBreakerState string
const (
StateClosed CircuitBreakerState = "CLOSED"
StateOpen CircuitBreakerState = "OPEN"
StateHalfOpen CircuitBreakerState = "HALF_OPEN"
)
// CircuitBreaker implements circuit breaker pattern
type CircuitBreaker struct {
state CircuitBreakerState
failureCount int
successCount int
failureThreshold int
successThreshold int
timeout time.Duration
lastFailureTime time.Time
mu sync.RWMutex
}
// NewCircuitBreaker creates a new circuit breaker
func NewCircuitBreaker(failureThreshold, successThreshold int, timeout time.Duration) *CircuitBreaker {
return &CircuitBreaker{
state: StateClosed,
failureThreshold: failureThreshold,
successThreshold: successThreshold,
timeout: timeout,
}
}
// Call executes a function with circuit breaker protection
func (cb *CircuitBreaker) Call(fn func() error) error {
cb.mu.Lock()
defer cb.mu.Unlock()
if cb.state == StateOpen {
if time.Since(cb.lastFailureTime) > cb.timeout {
cb.state = StateHalfOpen
cb.successCount = 0
} else {
return fmt.Errorf("circuit breaker is open")
}
}
err := fn()
if err != nil {
cb.failureCount++
cb.lastFailureTime = time.Now()
if cb.failureCount >= cb.failureThreshold {
cb.state = StateOpen
}
return err
}
cb.failureCount = 0
if cb.state == StateHalfOpen {
cb.successCount++
if cb.successCount >= cb.successThreshold {
cb.state = StateClosed
}
}
return nil
}
Best Practices
1. Logging and Monitoring
// Log all requests and responses
func (ag *APIGateway) logRequest(r *http.Request) {
log.Printf("%s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
}
2. Timeout Management
// Set appropriate timeouts
client := &http.Client{
Timeout: 30 * time.Second,
}
3. Error Handling
// Handle errors gracefully
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
4. Security
// Implement security measures
// - Authentication
// - Authorization
// - Rate limiting
// - Input validation
Common Pitfalls
1. No Rate Limiting
Always implement rate limiting to protect backend services.
2. Insufficient Logging
Log all requests for debugging and monitoring.
3. No Circuit Breaker
Implement circuit breakers to handle failing services.
4. Poor Error Handling
Handle errors gracefully and return appropriate status codes.
Resources
Summary
API gateways and reverse proxies are essential for microservices. Key takeaways:
- Route requests to appropriate backend services
- Implement authentication and authorization
- Use rate limiting to protect services
- Transform requests and responses as needed
- Implement circuit breakers for resilience
- Log all requests for monitoring
- Handle errors gracefully
- Monitor gateway performance
By implementing robust API gateways, you can build scalable, secure microservices architectures.
Comments