CrackedRuby CrackedRuby

Overview

The API Gateway pattern addresses the challenges that arise when client applications need to interact with multiple microservices in a distributed system. Rather than requiring clients to maintain connections to numerous services and understand their individual protocols and endpoints, the API Gateway pattern introduces a single intermediary component that sits between clients and backend services.

This pattern emerged as microservices architectures became prevalent. In monolithic applications, clients interact with a single application interface. When systems decompose into microservices, clients would otherwise need to manage multiple service endpoints, handle different protocols, implement retry logic for each service, and aggregate data from multiple sources. The API Gateway consolidates these responsibilities into a single component.

The gateway receives all client requests, performs necessary preprocessing, routes requests to appropriate backend services, aggregates responses when needed, and returns consolidated results to clients. This abstraction shields clients from the complexity of the backend architecture and provides a stable interface even as backend services evolve.

Consider an e-commerce application where a mobile client needs to display a product page. The client requires product details from the catalog service, pricing from the pricing service, inventory status from the warehouse service, and customer reviews from the reviews service. Without an API Gateway, the mobile app would make four separate HTTP requests, handle four different response formats, manage four sets of error conditions, and implement retry logic four times. With an API Gateway, the mobile app makes one request to /api/products/123, and the gateway handles all backend coordination.

# Without API Gateway - client makes multiple requests
product = http_get("https://catalog.service/products/123")
pricing = http_get("https://pricing.service/products/123")
inventory = http_get("https://warehouse.service/inventory/123")
reviews = http_get("https://reviews.service/products/123")

# With API Gateway - single request
product_page = http_get("https://api.gateway/products/123")
# Gateway handles all backend calls internally

Key Principles

The API Gateway operates on several foundational principles that define its behavior and responsibilities within a distributed system architecture.

Single Entry Point: All client requests flow through the gateway regardless of their destination backend service. This centralization applies to both public-facing APIs and internal service-to-service communication in some implementations. The gateway maintains routing tables that map incoming request paths to backend service endpoints.

Request Routing: The gateway examines incoming requests and directs them to appropriate backend services based on URL paths, HTTP methods, headers, or request content. Routing rules can include complex logic such as version-based routing, geographic routing, or canary deployments where a percentage of traffic goes to new service versions.

Protocol Translation: Backend services may use different communication protocols than clients expect. The gateway translates between protocols, accepting HTTP/REST requests from web clients while communicating with backend services via gRPC, GraphQL, or message queues. This translation extends to data formats, converting between JSON, Protocol Buffers, or XML as needed.

Request and Response Transformation: The gateway modifies request and response payloads to match client expectations. This includes field mapping, data aggregation from multiple services, filtering sensitive information, and reformatting data structures. The gateway might combine three service responses into a single JSON object or extract specific fields from a complex backend response.

Cross-Cutting Concerns: The gateway implements functionality required by all services rather than duplicating it across individual services. These concerns include authentication verification, authorization checks, rate limiting, request logging, metrics collection, and response caching. Centralizing these concerns reduces code duplication and ensures consistent implementation across services.

Response Aggregation: When a client request requires data from multiple backend services, the gateway fetches data from all necessary services, combines the responses according to business logic, and returns a unified response. This pattern reduces network round trips and simplifies client code.

# Gateway routing principle
class APIGateway
  def route(request)
    case request.path
    when %r{^/products}
      ProductService.handle(request)
    when %r{^/orders}
      OrderService.handle(request)
    when %r{^/users}
      UserService.handle(request)
    else
      [404, {}, ["Not Found"]]
    end
  end
end

# Response aggregation principle
class ProductGateway
  def get_product_details(product_id)
    product = catalog_service.get(product_id)
    pricing = pricing_service.get(product_id)
    inventory = warehouse_service.get(product_id)
    
    {
      id: product.id,
      name: product.name,
      price: pricing.current_price,
      in_stock: inventory.quantity > 0,
      available_quantity: inventory.quantity
    }
  end
end

Service Discovery Integration: The gateway maintains awareness of available backend services and their network locations. When services scale horizontally or move between hosts, the gateway updates its routing tables dynamically. This requires integration with service discovery mechanisms like Consul, Eureka, or Kubernetes services.

Circuit Breaking: The gateway monitors backend service health and prevents cascading failures. When a backend service fails repeatedly, the gateway opens a circuit breaker, immediately returning error responses without attempting to contact the failing service. After a timeout period, the gateway allows probe requests to determine if the service has recovered.

Design Considerations

Selecting the API Gateway pattern requires evaluating several architectural trade-offs and understanding when this pattern provides value versus adding unnecessary complexity.

When to Use an API Gateway: Microservices architectures with multiple client types benefit most from API Gateways. Mobile applications, web browsers, and third-party integrations often need different data shapes and levels of detail. A mobile app on a slow network requires minimal payloads, while an administrative web interface needs comprehensive data. The gateway tailors responses for each client type without requiring backend services to maintain multiple interfaces.

Systems exposing public APIs to external developers need consistent versioning, authentication, and rate limiting. The gateway enforces these policies uniformly rather than implementing them in each service. Organizations with compliance requirements for logging, audit trails, or data privacy find gateways simplify meeting these requirements by centralizing control points.

When to Avoid an API Gateway: Simple applications with few services or a single client type gain little from adding a gateway layer. The gateway introduces network latency, additional infrastructure complexity, and a potential single point of failure. Applications with latency-sensitive operations where every millisecond matters should minimize intermediary hops.

Systems where clients can directly access backend services without transformation or aggregation don't benefit from gateway overhead. Internal service-to-service communication within a microservices cluster often bypasses the gateway to reduce latency, using service mesh technologies instead.

Complexity Trade-offs: The API Gateway adds a component that requires development, deployment, monitoring, and maintenance. Teams must decide whether to build a custom gateway, adopt an open-source solution, or use a managed cloud service. Custom gateways offer maximum flexibility but require significant engineering investment. Open-source options like Kong, Tyk, or Traefik provide extensive features but need operational expertise. Managed services like AWS API Gateway or Google Cloud Endpoints reduce operational burden but introduce vendor lock-in and potential cost concerns at scale.

Scalability Implications: The gateway handles all client traffic, making it a critical component for system scalability. Gateways must scale horizontally to handle increasing load, requiring stateless design and session management external to gateway instances. Load balancers distribute traffic across gateway instances, and auto-scaling policies add or remove instances based on traffic patterns.

Backend services can scale independently based on their individual load patterns, but the gateway must scale to handle aggregate traffic from all clients. Monitoring gateway performance metrics like request latency, throughput, and error rates becomes critical for maintaining system health.

Failure Impact: A failing gateway affects all client requests regardless of backend service health. This creates a single point of failure unless the architecture includes redundancy. Deploying multiple gateway instances behind load balancers provides failover capability, but coordinated failures or deployment problems can still cause system-wide outages.

Circuit breakers and timeout configurations require careful tuning. Aggressive timeouts reduce latency when services fail but may cause premature failures during temporary slowdowns. Conservative timeouts keep connections open longer, potentially exhausting connection pools during cascading failures.

# Design consideration: Multiple gateway instances
class GatewayCluster
  def initialize(gateway_instances)
    @instances = gateway_instances
    @load_balancer = RoundRobinBalancer.new(gateway_instances)
  end
  
  def handle_request(request)
    instance = @load_balancer.next_instance
    instance.process(request)
  rescue ServiceUnavailable
    # Failover to another instance
    @instances.reject { |i| i == instance }.sample.process(request)
  end
end

Data Consistency: Gateways that cache responses improve performance but introduce eventual consistency concerns. Cached data becomes stale when backend services update their state. Cache invalidation strategies range from time-based expiration to event-driven updates. Applications requiring strong consistency should avoid caching or implement cache invalidation carefully.

Security Boundaries: The gateway represents a trust boundary between external clients and internal services. All security validations must occur at the gateway because backend services trust gateway requests. Deploying backend services in private networks unreachable from the internet reinforces this boundary. The gateway becomes the sole entry point, simplifying firewall rules and access control.

Ruby Implementation

Ruby offers several approaches for implementing API Gateways, from Rack-based middleware to full-featured frameworks. The choice depends on system requirements, team expertise, and integration needs.

Rack-Based Gateway: Rack provides the foundation for Ruby web applications and serves as an excellent base for lightweight gateways. A Rack application receives requests and returns responses, making it straightforward to implement routing and forwarding logic.

require 'rack'
require 'net/http'
require 'json'

class SimpleGateway
  def initialize
    @routes = {
      '/api/products' => 'http://products-service:3000',
      '/api/orders' => 'http://orders-service:3001',
      '/api/users' => 'http://users-service:3002'
    }
  end
  
  def call(env)
    request = Rack::Request.new(env)
    path = request.path
    
    service_url = find_service_url(path)
    return [404, {}, ['Service not found']] unless service_url
    
    forward_request(service_url, request)
  end
  
  private
  
  def find_service_url(path)
    @routes.each do |route, url|
      return url if path.start_with?(route)
    end
    nil
  end
  
  def forward_request(service_url, request)
    uri = URI("#{service_url}#{request.path}")
    uri.query = request.query_string unless request.query_string.empty?
    
    http = Net::HTTP.new(uri.host, uri.port)
    http_request = Net::HTTP::Get.new(uri)
    
    # Forward headers
    request.each_header do |key, value|
      http_request[key] = value if key.start_with?('HTTP_')
    end
    
    response = http.request(http_request)
    
    [response.code.to_i, response.to_hash, [response.body]]
  rescue StandardError => e
    [503, {'Content-Type' => 'application/json'}, 
     [JSON.generate({error: 'Service unavailable', details: e.message})]]
  end
end

# Run with: rackup -p 8080
run SimpleGateway.new

Sinatra-Based Gateway: Sinatra provides routing DSL and middleware support, making it suitable for gateways requiring more sophisticated routing logic and request processing.

require 'sinatra/base'
require 'faraday'
require 'circuit_breaker'

class APIGateway < Sinatra::Base
  configure do
    set :service_clients, {
      products: create_client('http://products-service:3000'),
      orders: create_client('http://orders-service:3001'),
      users: create_client('http://users-service:3002')
    }
    
    set :circuit_breakers, Hash.new { |h, k| 
      h[k] = CircuitBreaker.new(threshold: 5, timeout: 30)
    }
  end
  
  before do
    authenticate_request
    check_rate_limit
  end
  
  get '/api/products/:id' do
    product_id = params[:id]
    
    # Aggregate data from multiple services
    circuit_breaker = settings.circuit_breakers[:products]
    
    result = circuit_breaker.call do
      product = fetch_product(product_id)
      pricing = fetch_pricing(product_id)
      inventory = fetch_inventory(product_id)
      
      {
        product: product,
        pricing: pricing,
        inventory: inventory
      }
    end
    
    json result
  rescue CircuitBreaker::OpenError
    status 503
    json error: 'Product service temporarily unavailable'
  end
  
  get '/api/orders' do
    client = settings.service_clients[:orders]
    response = client.get('/orders', user_id: current_user.id)
    
    status response.status
    response.body
  end
  
  private
  
  def authenticate_request
    token = request.env['HTTP_AUTHORIZATION']
    halt 401, json(error: 'Unauthorized') unless valid_token?(token)
    @current_user = User.from_token(token)
  end
  
  def check_rate_limit
    limiter = RateLimiter.new(redis: Redis.current)
    allowed = limiter.allow?(current_user.id, limit: 100, period: 60)
    halt 429, json(error: 'Rate limit exceeded') unless allowed
  end
  
  def fetch_product(id)
    client = settings.service_clients[:products]
    response = client.get("/products/#{id}")
    JSON.parse(response.body)
  end
  
  def fetch_pricing(id)
    client = settings.service_clients[:products]
    response = client.get("/pricing/#{id}")
    JSON.parse(response.body)
  end
  
  def fetch_inventory(id)
    client = settings.service_clients[:products]
    response = client.get("/inventory/#{id}")
    JSON.parse(response.body)
  end
  
  def current_user
    @current_user
  end
  
  def self.create_client(base_url)
    Faraday.new(url: base_url) do |conn|
      conn.request :json
      conn.response :json
      conn.adapter Faraday.default_adapter
      conn.options.timeout = 5
      conn.options.open_timeout = 2
    end
  end
end

Rails-Based Gateway: Rails applications can act as API Gateways, particularly when the gateway requires complex business logic, database access, or integration with existing Rails infrastructure.

# config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    resources :products, only: [:index, :show]
    resources :orders, only: [:index, :show, :create]
  end
end

# app/controllers/api/base_controller.rb
class Api::BaseController < ApplicationController
  before_action :authenticate_api_request
  before_action :check_rate_limit
  
  rescue_from ServiceUnavailableError, with: :service_unavailable
  rescue_from CircuitBreakerOpenError, with: :circuit_breaker_open
  
  private
  
  def authenticate_api_request
    token = request.headers['Authorization']
    @current_user = UserAuthenticator.authenticate(token)
    render json: {error: 'Unauthorized'}, status: 401 unless @current_user
  end
  
  def check_rate_limit
    limiter = RateLimiter.new(key: "user:#{@current_user.id}")
    unless limiter.allow?(limit: 1000, period: 1.hour)
      render json: {error: 'Rate limit exceeded'}, status: 429
    end
  end
  
  def service_unavailable(exception)
    render json: {error: 'Service temporarily unavailable'}, status: 503
  end
  
  def circuit_breaker_open(exception)
    render json: {error: 'Service circuit breaker open'}, status: 503
  end
end

# app/controllers/api/products_controller.rb
class Api::ProductsController < Api::BaseController
  def show
    product_id = params[:id]
    
    result = Rails.cache.fetch("product:#{product_id}", expires_in: 5.minutes) do
      ProductServiceClient.get_product_details(product_id)
    end
    
    render json: result
  end
end

# app/services/product_service_client.rb
class ProductServiceClient
  def self.get_product_details(product_id)
    circuit_breaker = CircuitBreakerRegistry.get(:products)
    
    circuit_breaker.call do
      product = fetch_from_service('/products', product_id)
      pricing = fetch_from_service('/pricing', product_id)
      inventory = fetch_from_service('/inventory', product_id)
      reviews = fetch_from_service('/reviews', product_id)
      
      aggregate_response(product, pricing, inventory, reviews)
    end
  end
  
  private
  
  def self.fetch_from_service(path, id)
    response = Faraday.get("#{ENV['PRODUCT_SERVICE_URL']}#{path}/#{id}")
    JSON.parse(response.body)
  rescue Faraday::Error => e
    Rails.logger.error("Service request failed: #{e.message}")
    raise ServiceUnavailableError
  end
  
  def self.aggregate_response(product, pricing, inventory, reviews)
    {
      id: product['id'],
      name: product['name'],
      description: product['description'],
      price: pricing['current_price'],
      original_price: pricing['original_price'],
      in_stock: inventory['quantity'] > 0,
      quantity: inventory['quantity'],
      rating: reviews['average_rating'],
      review_count: reviews['count']
    }
  end
end

Kong Gateway with Ruby: Kong is an open-source API Gateway that can be extended with plugins written in Lua or run alongside Ruby services that handle custom business logic.

# Custom Ruby service that Kong forwards to
require 'sinatra/base'
require 'jwt'

class CustomAuthService < Sinatra::Base
  post '/validate' do
    token = request.env['HTTP_AUTHORIZATION']&.split(' ')&.last
    
    begin
      payload = JWT.decode(token, ENV['JWT_SECRET'], true, algorithm: 'HS256')
      user_id = payload[0]['user_id']
      
      # Add custom headers for downstream services
      headers 'X-User-Id' => user_id.to_s
      headers 'X-User-Role' => fetch_user_role(user_id)
      
      status 200
      json valid: true, user_id: user_id
    rescue JWT::DecodeError
      status 401
      json valid: false, error: 'Invalid token'
    end
  end
  
  private
  
  def fetch_user_role(user_id)
    # Query user database or cache
    user = User.find(user_id)
    user.role
  end
end

Practical Examples

Mobile Backend Gateway: A mobile application requires optimized payloads and handles spotty network connections. The gateway adapts backend responses for mobile constraints.

class MobileAPIGateway < Sinatra::Base
  # Mobile-optimized endpoint that returns minimal data
  get '/mobile/v1/feed' do
    user_id = current_user.id
    
    # Fetch data from multiple services
    posts = fetch_posts(user_id, limit: 20)
    users = fetch_user_profiles(posts.map(&:author_id))
    media = fetch_media_thumbnails(posts.map(&:media_ids).flatten)
    
    # Transform to mobile-optimized format
    feed = posts.map do |post|
      {
        id: post.id,
        text: truncate(post.text, 200),
        author: {
          id: users[post.author_id].id,
          name: users[post.author_id].name,
          avatar_url: users[post.author_id].avatar_thumbnail
        },
        media: post.media_ids.map { |id| media[id].thumbnail_url },
        timestamp: post.created_at.iso8601
      }
    end
    
    # Cache for mobile offline support
    headers 'Cache-Control' => 'max-age=300, stale-while-revalidate=600'
    
    json feed: feed, next_page: posts.last&.id
  end
  
  # Batch endpoint to reduce round trips
  post '/mobile/v1/batch' do
    requests = JSON.parse(request.body.read)
    
    results = requests.map do |req|
      process_batch_request(req)
    end
    
    json results
  end
  
  private
  
  def process_batch_request(req)
    case req['endpoint']
    when 'posts'
      fetch_posts(current_user.id, limit: req['limit'])
    when 'notifications'
      fetch_notifications(current_user.id)
    when 'messages'
      fetch_messages(current_user.id)
    end
  rescue StandardError => e
    {error: e.message, endpoint: req['endpoint']}
  end
end

Backend for Frontend (BFF) Pattern: Different client types require different gateway implementations. A web dashboard needs comprehensive data while a mobile app needs minimal payloads.

# Web BFF - comprehensive data
class WebBFFGateway < Sinatra::Base
  get '/web/dashboard' do
    user_id = current_user.id
    
    # Fetch comprehensive dashboard data
    dashboard = {
      user: fetch_complete_user_profile(user_id),
      recent_orders: fetch_orders(user_id, limit: 50),
      analytics: fetch_analytics(user_id),
      recommendations: fetch_recommendations(user_id, limit: 20),
      notifications: fetch_all_notifications(user_id)
    }
    
    json dashboard
  end
end

# Mobile BFF - minimal data
class MobileBFFGateway < Sinatra::Base
  get '/mobile/dashboard' do
    user_id = current_user.id
    
    # Fetch minimal dashboard data
    dashboard = {
      user: {
        id: user_id,
        name: fetch_user_name(user_id)
      },
      recent_orders: fetch_orders(user_id, limit: 5).map(&:summary),
      unread_notifications: fetch_unread_count(user_id)
    }
    
    json dashboard
  end
end

Microservices Orchestration: The gateway coordinates complex workflows spanning multiple services.

class CheckoutGateway < Sinatra::Base
  post '/api/checkout' do
    order_data = JSON.parse(request.body.read)
    user_id = current_user.id
    
    # Orchestrate checkout workflow
    result = process_checkout(user_id, order_data)
    
    if result[:success]
      status 201
      json order: result[:order], payment: result[:payment]
    else
      status 422
      json errors: result[:errors]
    end
  end
  
  private
  
  def process_checkout(user_id, order_data)
    # Step 1: Validate inventory
    inventory_check = InventoryService.check_availability(order_data[:items])
    return {success: false, errors: ['Items unavailable']} unless inventory_check[:available]
    
    # Step 2: Calculate pricing with promotions
    pricing = PricingService.calculate(order_data[:items], user_id)
    
    # Step 3: Reserve inventory
    reservation = InventoryService.reserve(order_data[:items], ttl: 600)
    
    begin
      # Step 4: Process payment
      payment = PaymentService.charge(
        user_id: user_id,
        amount: pricing[:total],
        method: order_data[:payment_method]
      )
      
      # Step 5: Create order
      order = OrderService.create(
        user_id: user_id,
        items: order_data[:items],
        payment_id: payment[:id],
        total: pricing[:total]
      )
      
      # Step 6: Confirm inventory reservation
      InventoryService.confirm_reservation(reservation[:id])
      
      # Step 7: Send notifications
      NotificationService.send_order_confirmation(user_id, order[:id])
      
      {success: true, order: order, payment: payment}
    rescue PaymentError => e
      # Rollback inventory reservation
      InventoryService.release_reservation(reservation[:id])
      {success: false, errors: ["Payment failed: #{e.message}"]}
    rescue StandardError => e
      # Rollback all operations
      InventoryService.release_reservation(reservation[:id])
      PaymentService.refund(payment[:id]) if payment
      {success: false, errors: ["Checkout failed: #{e.message}"]}
    end
  end
end

Security Implications

API Gateways serve as the primary security enforcement point in microservices architectures, requiring careful implementation of multiple security layers.

Authentication and Authorization: The gateway verifies client identity and checks permissions before forwarding requests to backend services. This prevents unauthorized access and ensures backend services can trust incoming requests.

class SecureGateway < Sinatra::Base
  before do
    authenticate_request
    authorize_request
  end
  
  private
  
  def authenticate_request
    auth_header = request.env['HTTP_AUTHORIZATION']
    
    if auth_header&.start_with?('Bearer ')
      token = auth_header.split(' ').last
      verify_jwt_token(token)
    elsif auth_header&.start_with?('ApiKey ')
      api_key = auth_header.split(' ').last
      verify_api_key(api_key)
    else
      halt 401, json(error: 'Missing authentication')
    end
  end
  
  def verify_jwt_token(token)
    payload = JWT.decode(token, ENV['JWT_SECRET'], true, algorithm: 'HS256')
    @current_user = User.find(payload[0]['user_id'])
    @auth_context = {
      user_id: @current_user.id,
      roles: payload[0]['roles'],
      scopes: payload[0]['scopes']
    }
  rescue JWT::DecodeError, JWT::ExpiredSignature
    halt 401, json(error: 'Invalid or expired token')
  end
  
  def verify_api_key(key)
    api_key = ApiKey.find_by(key: key)
    halt 401, json(error: 'Invalid API key') unless api_key&.active?
    
    @auth_context = {
      api_key_id: api_key.id,
      client_id: api_key.client_id,
      scopes: api_key.scopes
    }
  end
  
  def authorize_request
    required_scope = route_scope(request.path, request.request_method)
    has_scope = @auth_context[:scopes].include?(required_scope)
    
    halt 403, json(error: 'Insufficient permissions') unless has_scope
  end
  
  def route_scope(path, method)
    case [path, method]
    when [%r{^/api/products}, 'GET']
      'read:products'
    when [%r{^/api/products}, 'POST']
      'write:products'
    when [%r{^/api/orders}, 'GET']
      'read:orders'
    when [%r{^/api/orders}, 'POST']
      'write:orders'
    else
      'unknown'
    end
  end
end

Rate Limiting and Throttling: The gateway prevents abuse by limiting request rates per client, protecting backend services from overload.

class RateLimiter
  def initialize(redis:)
    @redis = redis
  end
  
  def allow?(client_id:, limit:, period:)
    key = "rate_limit:#{client_id}:#{Time.now.to_i / period}"
    count = @redis.incr(key)
    @redis.expire(key, period * 2) if count == 1
    
    count <= limit
  end
  
  def remaining(client_id:, limit:, period:)
    key = "rate_limit:#{client_id}:#{Time.now.to_i / period}"
    count = @redis.get(key).to_i
    [limit - count, 0].max
  end
end

class GatewayWithRateLimiting < Sinatra::Base
  before do
    limiter = RateLimiter.new(redis: Redis.current)
    client_id = auth_context[:user_id] || auth_context[:api_key_id]
    
    limit = determine_limit(auth_context)
    period = 60 # 1 minute
    
    unless limiter.allow?(client_id: client_id, limit: limit, period: period)
      remaining = limiter.remaining(client_id: client_id, limit: limit, period: period)
      headers 'X-RateLimit-Remaining' => remaining.to_s
      headers 'X-RateLimit-Reset' => ((Time.now.to_i / period + 1) * period).to_s
      
      halt 429, json(error: 'Rate limit exceeded')
    end
  end
  
  def determine_limit(context)
    return 1000 if context[:roles]&.include?('premium')
    return 100 if context[:roles]&.include?('basic')
    10 # Default for unauthenticated
  end
end

Input Validation and Sanitization: The gateway validates all request data before forwarding to backend services, preventing injection attacks and malformed requests.

class InputValidator
  def self.validate_product_id(id)
    return nil unless id =~ /\A\d+\z/
    id.to_i
  end
  
  def self.validate_email(email)
    return nil unless email =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
    email.downcase
  end
  
  def self.sanitize_search_query(query)
    # Remove potentially dangerous characters
    query.gsub(/[<>'"\\]/, '')
  end
end

class ValidatingGateway < Sinatra::Base
  get '/api/products/:id' do
    product_id = InputValidator.validate_product_id(params[:id])
    halt 400, json(error: 'Invalid product ID') unless product_id
    
    # Forward validated request
    response = ProductService.get(product_id)
    json response
  end
  
  post '/api/users' do
    data = JSON.parse(request.body.read)
    
    email = InputValidator.validate_email(data['email'])
    halt 400, json(error: 'Invalid email') unless email
    
    # Additional validations
    halt 400, json(error: 'Name too long') if data['name'].length > 100
    halt 400, json(error: 'Invalid age') if data['age'] && (data['age'] < 0 || data['age'] > 150)
    
    response = UserService.create(email: email, name: data['name'], age: data['age'])
    status 201
    json response
  end
end

TLS Termination: The gateway handles SSL/TLS connections, decrypting incoming requests and encrypting responses, while communicating with backend services over a secure internal network.

CORS Management: The gateway implements Cross-Origin Resource Sharing policies for browser-based clients.

class CORSGateway < Sinatra::Base
  before do
    headers 'Access-Control-Allow-Origin' => allowed_origin
    headers 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS'
    headers 'Access-Control-Allow-Headers' => 'Content-Type, Authorization'
    headers 'Access-Control-Max-Age' => '86400'
  end
  
  options '*' do
    200
  end
  
  private
  
  def allowed_origin
    origin = request.env['HTTP_ORIGIN']
    whitelist = ENV['ALLOWED_ORIGINS'].split(',')
    whitelist.include?(origin) ? origin : whitelist.first
  end
end

Performance Considerations

Gateway performance directly impacts overall system responsiveness. Several strategies optimize gateway throughput and latency.

Caching Strategies: The gateway caches responses from backend services to reduce load and improve response times. Cache strategies include time-based expiration, cache invalidation on updates, and conditional requests.

class CachingGateway < Sinatra::Base
  configure do
    set :cache, Redis.new(url: ENV['REDIS_URL'])
  end
  
  get '/api/products/:id' do
    product_id = params[:id]
    cache_key = "product:#{product_id}"
    
    # Try cache first
    cached = settings.cache.get(cache_key)
    if cached
      headers 'X-Cache' => 'HIT'
      return cached
    end
    
    # Fetch from service
    response = ProductService.get(product_id)
    
    # Cache for 5 minutes
    settings.cache.setex(cache_key, 300, response.to_json)
    
    headers 'X-Cache' => 'MISS'
    json response
  end
  
  # Invalidate cache on updates
  put '/api/products/:id' do
    product_id = params[:id]
    data = JSON.parse(request.body.read)
    
    response = ProductService.update(product_id, data)
    
    # Invalidate cache
    cache_key = "product:#{product_id}"
    settings.cache.del(cache_key)
    
    json response
  end
end

Connection Pooling: The gateway maintains connection pools to backend services, avoiding connection establishment overhead for each request.

class ConnectionPoolGateway
  def initialize
    @pools = {}
    configure_pools
  end
  
  def configure_pools
    @pools[:products] = ConnectionPool.new(size: 20, timeout: 5) do
      Faraday.new(url: ENV['PRODUCT_SERVICE_URL']) do |conn|
        conn.adapter :net_http_persistent
        conn.options.timeout = 10
      end
    end
    
    @pools[:orders] = ConnectionPool.new(size: 20, timeout: 5) do
      Faraday.new(url: ENV['ORDER_SERVICE_URL']) do |conn|
        conn.adapter :net_http_persistent
        conn.options.timeout = 10
      end
    end
  end
  
  def get_product(id)
    @pools[:products].with do |client|
      response = client.get("/products/#{id}")
      JSON.parse(response.body)
    end
  end
end

Parallel Requests: When aggregating data from multiple services, the gateway executes requests in parallel rather than sequentially.

require 'concurrent'

class ParallelGateway < Sinatra::Base
  get '/api/dashboard' do
    user_id = current_user.id
    
    # Execute requests in parallel
    futures = {
      profile: Concurrent::Future.execute { fetch_profile(user_id) },
      orders: Concurrent::Future.execute { fetch_orders(user_id) },
      recommendations: Concurrent::Future.execute { fetch_recommendations(user_id) },
      notifications: Concurrent::Future.execute { fetch_notifications(user_id) }
    }
    
    # Wait for all futures to complete
    results = futures.transform_values(&:value)
    
    json results
  rescue StandardError => e
    status 500
    json error: 'Dashboard data unavailable'
  end
end

Request Compression: The gateway compresses response bodies for bandwidth-constrained clients.

class CompressionGateway < Sinatra::Base
  use Rack::Deflater
  
  get '/api/large-dataset' do
    data = fetch_large_dataset
    
    # Rack::Deflater handles compression based on Accept-Encoding header
    json data
  end
end

Load Balancing: Multiple gateway instances distribute traffic, and the gateway distributes requests across backend service instances.

class LoadBalancedGateway
  def initialize
    @service_instances = {
      products: ['http://products-1:3000', 'http://products-2:3000', 'http://products-3:3000'],
      orders: ['http://orders-1:3001', 'http://orders-2:3001']
    }
    @current_index = Hash.new(0)
  end
  
  def route_request(service_name, path)
    instances = @service_instances[service_name]
    
    # Round-robin load balancing
    index = @current_index[service_name]
    instance = instances[index]
    @current_index[service_name] = (index + 1) % instances.length
    
    # Make request to selected instance
    response = Faraday.get("#{instance}#{path}")
    response.body
  rescue Faraday::Error
    # Try next instance on failure
    retry_with_next_instance(service_name, path, instance)
  end
  
  def retry_with_next_instance(service_name, path, failed_instance)
    instances = @service_instances[service_name].reject { |i| i == failed_instance }
    return nil if instances.empty?
    
    instance = instances.first
    response = Faraday.get("#{instance}#{path}")
    response.body
  end
end

Common Patterns

Several recurring patterns emerge in API Gateway implementations, each addressing specific architectural needs.

Backend for Frontend (BFF): Each client type has a dedicated gateway optimized for its needs. Mobile apps, web applications, and third-party integrations each interact with separate BFF gateways.

# Shared base gateway
class BaseGateway < Sinatra::Base
  helpers do
    def fetch_user_data(user_id)
      UserService.get(user_id)
    end
    
    def fetch_orders(user_id, limit: 10)
      OrderService.list(user_id: user_id, limit: limit)
    end
  end
end

# Mobile BFF - minimal data, optimized payloads
class MobileBFF < BaseGateway
  get '/home' do
    user_data = fetch_user_data(current_user.id)
    recent_orders = fetch_orders(current_user.id, limit: 5)
    
    json(
      user: {id: user_data.id, name: user_data.name},
      orders: recent_orders.map { |o| {id: o.id, status: o.status, total: o.total} }
    )
  end
end

# Web BFF - comprehensive data
class WebBFF < BaseGateway
  get '/home' do
    user_data = fetch_user_data(current_user.id)
    recent_orders = fetch_orders(current_user.id, limit: 20)
    
    json(
      user: user_data,
      orders: recent_orders,
      analytics: fetch_analytics(current_user.id),
      recommendations: fetch_recommendations(current_user.id)
    )
  end
end

GraphQL Gateway: The gateway exposes a GraphQL API while backend services use REST or other protocols.

require 'graphql'

class Schema < GraphQL::Schema
  class Product < GraphQL::Schema::Object
    field :id, ID, null: false
    field :name, String, null: false
    field :price, Float, null: false
    field :inventory, Integer, null: false
  end
  
  class Query < GraphQL::Schema::Object
    field :product, Product, null: false do
      argument :id, ID, required: true
    end
    
    def product(id:)
      product_data = ProductService.get(id)
      pricing_data = PricingService.get(id)
      inventory_data = InventoryService.get(id)
      
      {
        id: product_data['id'],
        name: product_data['name'],
        price: pricing_data['current_price'],
        inventory: inventory_data['quantity']
      }
    end
  end
  
  query Query
end

class GraphQLGateway < Sinatra::Base
  post '/graphql' do
    query = params[:query]
    variables = JSON.parse(params[:variables] || '{}')
    
    result = Schema.execute(query, variables: variables, context: {current_user: current_user})
    json result
  end
end

Aggregator Pattern: The gateway combines responses from multiple services into a single response, reducing client round trips.

class AggregatorGateway < Sinatra::Base
  get '/api/order-details/:id' do
    order_id = params[:id]
    
    # Fetch from multiple services
    order = OrderService.get(order_id)
    customer = CustomerService.get(order['customer_id'])
    products = order['items'].map { |item| ProductService.get(item['product_id']) }
    shipping = ShippingService.get_status(order['shipping_id'])
    
    # Aggregate into single response
    json(
      order: {
        id: order['id'],
        date: order['created_at'],
        status: order['status'],
        total: order['total']
      },
      customer: {
        name: customer['name'],
        email: customer['email']
      },
      items: products.map.with_index do |product, index|
        {
          product_name: product['name'],
          quantity: order['items'][index]['quantity'],
          price: order['items'][index]['price']
        }
      end,
      shipping: {
        status: shipping['status'],
        estimated_delivery: shipping['estimated_delivery']
      }
    )
  end
end

Chain Pattern: The gateway processes requests through a chain of handlers, each performing specific transformations or validations.

class Handler
  attr_accessor :next_handler
  
  def handle(request, response)
    raise NotImplementedError
  end
  
  def call_next(request, response)
    @next_handler&.handle(request, response)
  end
end

class AuthenticationHandler < Handler
  def handle(request, response)
    unless authenticate(request)
      response[:error] = 'Authentication failed'
      return response
    end
    
    call_next(request, response)
  end
end

class ValidationHandler < Handler
  def handle(request, response)
    unless validate(request)
      response[:error] = 'Validation failed'
      return response
    end
    
    call_next(request, response)
  end
end

class RateLimitHandler < Handler
  def handle(request, response)
    unless check_rate_limit(request)
      response[:error] = 'Rate limit exceeded'
      return response
    end
    
    call_next(request, response)
  end
end

class RouteHandler < Handler
  def handle(request, response)
    service_response = route_to_service(request)
    response[:data] = service_response
    response
  end
end

class ChainGateway < Sinatra::Base
  configure do
    auth = AuthenticationHandler.new
    validation = ValidationHandler.new
    rate_limit = RateLimitHandler.new
    route = RouteHandler.new
    
    auth.next_handler = validation
    validation.next_handler = rate_limit
    rate_limit.next_handler = route
    
    set :handler_chain, auth
  end
  
  post '/api/*' do
    request_data = {path: params[:splat].first, body: request.body.read}
    response_data = {}
    
    result = settings.handler_chain.handle(request_data, response_data)
    
    if result[:error]
      status 400
      json error: result[:error]
    else
      json result[:data]
    end
  end
end

Reference

Core Components

Component Description Responsibility
Router Maps requests to backend services Path matching, service selection
Request Transformer Modifies request format Protocol translation, header manipulation
Response Aggregator Combines multiple service responses Data merging, format standardization
Authentication Module Verifies client identity Token validation, credential checking
Authorization Module Checks access permissions Role verification, scope validation
Rate Limiter Controls request frequency Traffic throttling, quota enforcement
Circuit Breaker Prevents cascading failures Failure detection, fallback handling
Cache Layer Stores frequently accessed data Response caching, cache invalidation
Load Balancer Distributes traffic across instances Instance selection, health checking

Gateway Deployment Patterns

Pattern Use Case Trade-offs
Single Gateway Simple architectures, unified policies Single point of failure, scaling limitations
Per-Client Gateway (BFF) Multiple client types with different needs Increased complexity, better optimization
Federated Gateway Large organizations, multiple teams Distributed ownership, coordination overhead
Service Mesh + Gateway Complex microservices deployments Advanced features, operational complexity

Caching Strategies

Strategy Implementation Invalidation
Time-Based Cache with TTL expiration Automatic after timeout
Event-Based Cache until explicit invalidation On update events
Conditional Cache with ETag/Last-Modified Client validates freshness
Distributed Shared cache across gateway instances Coordinated invalidation

Authentication Methods

Method Format Use Case
JWT Bearer token in Authorization header Stateless authentication
API Key ApiKey token in Authorization header Third-party integrations
OAuth 2.0 Bearer access token Delegated authorization
mTLS Client certificate Service-to-service communication

Rate Limiting Algorithms

Algorithm Characteristics Implementation
Token Bucket Smooth rate limiting, burst handling Tokens refill at constant rate
Leaky Bucket Strict rate enforcement Requests processed at fixed rate
Fixed Window Simple counter per time window Resets at window boundary
Sliding Window Precise rate limiting Tracks request timestamps

Circuit Breaker States

State Behavior Transition
Closed Normal operation, requests forwarded Opens on threshold failures
Open Fails fast, no requests forwarded Moves to half-open after timeout
Half-Open Probe requests allowed Closes on success, opens on failure

Ruby Gateway Gems

Gem Purpose Features
rack Base HTTP interface Middleware support, request/response handling
sinatra Lightweight web framework Routing DSL, middleware integration
rails Full-featured framework Complete ecosystem, extensive tooling
faraday HTTP client Connection pooling, middleware support
typhoeus Parallel HTTP requests Concurrent request execution
redis Caching and rate limiting Fast in-memory data store

Monitoring Metrics

Metric Purpose Threshold
Request Latency Response time measurement p95 < 200ms, p99 < 500ms
Throughput Requests per second Application dependent
Error Rate Failed request percentage < 1% target
Circuit Breaker State Service health indicator Track open circuits
Cache Hit Rate Cache effectiveness > 80% target
Connection Pool Usage Resource utilization < 80% average