Skip to main content
โšก Calmops

API Gateways and Reverse Proxies in Go

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