API Gateways

The Front Door of your System

The Problem: Direct Client-to-Microservice

Imagine our "Practice Manager" Mobile App. To show the Dashboard, it needs:

  1. Patient Name (Patient Service)
  2. Next Appointment (Consultation Service)
  3. Unpaid Invoices (Accounting Service)

If the App calls these directly:

  • Chatty: 3 requests over 4G/5G = High Latency.
  • Complex: Every service needs public IP & SSL config. App needs to know all URLs.
  • Coupled: If Patient Service moves to a new host, the Mobile App needs an update.

The Solution: API Gateway

An API Gateway is a server that acts as the single entry point into the system.

Analogy: The Hospital Receptionist.
You don't walk into the Operating Theater looking for a doctor. You ask the receptionist, who checks your credentials and points you to the right room.

Core Responsibilities

  1. Routing: GET /patients -> Service A, GET /invoices -> Service B.
  2. Offloading (Cross-Cutting Concerns):
    • SSL Termination: Manage certificates in one place.
    • Authentication: Check JWT/API Keys once.
    • Rate Limiting: Protect services from DdoS.
  3. Protocol Translation: HTTP -> gRPC, or HTTP -> AMQP.

The BFF Pattern (Backend For Frontend)

Sometimes, a general Gateway isn't enough.

  • Desktop Web: Needs heavy data tables.
  • Mobile App: Needs lightweight JSON.

Solution: Create specific Gateways for specific UI experiences.
This allows the API to evolve based on UI needs without changing the core microservices.

Introduction to Apache APISIX

  • High Performance: Based on Nginx and OpenResty (Lua).
  • Dynamic: Routes can be changed without restarting (Hot-reloading).
  • Plugin Architecture: 50+ built-in plugins (Auth, Limit-req, Zipkin, etc.).

Key APISIX Concepts

Route
Matches the incoming request (URI, Method, Host) and sends it to an Upstream.

Upstream
The backend service (e.g., patient-service:8080). Can perform load balancing (Round Robin).

Plugin
"Interceptors" that run before or after the request hits the Upstream (e.g., check Token, limit rate).

Hands-On Setup: Running APISIX

services:
  etcd:
    image: quay.io/coreos/etcd:v3.5.9
    container_name: microservices-etcd
    command:
      - /usr/local/bin/etcd
      - --enable-v2=true
      - --listen-client-urls=http://0.0.0.0:2379
      - --advertise-client-urls=http://0.0.0.0:2379
    ports:
      - "2379:2379"
    healthcheck:
      test: ["CMD", "/usr/local/bin/etcdctl", "endpoint", "health"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s

  apisix:
    image: docker.io/apache/apisix:3.7.0-debian
    container_name: microservices-apisix
    ports:
      - "9080:9080"   # HTTP
      - "9180:9180"   # Admin API
    volumes:
      - ./apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:Z
    depends_on:
      etcd:
        condition: service_healthy

APISIX Configuration File

apisix:
  node_listen: 9080
  enable_admin: true
  admin_key:
    - name: "admin"
      key: edd1c9f034335f136f87ad84b625c8f1
      role: admin

deployment:
  role: traditional
  role_traditional:
    config_provider: etcd
  admin:
    allow_admin:
      - 0.0.0.0/0
    admin_listen:
      ip: 0.0.0.0
      port: 9180
  etcd:
    host:
      - "http://microservices-etcd:2379"
    prefix: "/apisix"
    timeout: 30

plugin_attr:
  prometheus:
    export_addr:
      ip: "0.0.0.0"
      port: 9091

Setup Steps

1. Create directory structure:

mkdir -p apisix_conf

2. Create the config file:

cat > apisix_conf/config.yaml << 'EOF'
# Paste the config from previous slide
EOF

3. Start services:

docker-compose up -d

4. Verify:

curl http://localhost:9180/apisix/admin/routes \
  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'

Exercise 1: Basic Routing

Setup API Gateway for Practice Manager

Objective: Route requests to practicemanager and consultations services through APISIX.

Prerequisites:

  • Both microservices running (ports 8080, 8084)
  • APISIX running (port 9080)

Exercise 1: Steps (1/3)

Step 1: Create Route for Patient Service

curl http://127.0.0.1:9180/apisix/admin/routes/1 \
  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
  -X PUT -d '{
    "uris": ["/api/patients", "/api/patients/*"],
    "upstream": {
      "type": "roundrobin",
      "nodes": {
        "practicemanager:8080": 1
      }
    }
  }'

Exercise 1: Steps (2/3)

Step 2: Create Route for Consultation Service

curl http://127.0.0.1:9180/apisix/admin/routes/2 \
  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
  -X PUT -d '{
    "uris": ["/consultations", "/consultations/*"],
    "upstream": {
      "type": "roundrobin",
      "nodes": {
        "consultations:8084": 1
      }
    }
  }'

Exercise 1: Steps (3/3)

Step 3: Test the Routes

# Test patient service through gateway
curl http://localhost:9080/api/patients ... (example in README.md)

# Test consultation service through gateway
curl http://localhost:9080/consultations ... (example in README.md)

# Compare: Direct access still works
curl http://localhost:8080/api/patients ... 

Expected Result: Gateway routes requests correctly to backend services.

Exercise 2: Rate Limiting

Protect Your Services from Abuse

Objective: Add rate limiting to prevent DDoS and abuse.

Scenario: Limit patient searches to 10 requests per minute per IP.

Exercise 2: Implementation

curl http://127.0.0.1:9180/apisix/admin/routes/1 \
  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
  -X PUT -d '{
    "uris": ["/api/patients", "/api/patients/*"],
    "plugins": {
      "limit-count": {
        "count": 10,
        "time_window": 60,
        "rejected_code": 429,
        "rejected_msg": "Too many requests"
      }
    },
    "upstream": {
      "type": "roundrobin",
      "nodes": {"practicemanager:8080": 1}
    }
  }'

Exercise 2: Testing

# Make 15 requests quickly
for i in {1..15}; do
  curl -i http://localhost:9080/api/patients/P6CDDC602
  echo "Request $i"
done

Expected Result:

  • First 10 requests: HTTP 200
  • Next 5 requests: HTTP 429 (Too Many Requests)

Discussion: How would you implement per-user rate limiting instead of per-IP?

Exercise 3: Request/Response Transformation

Adapting APIs for Different Clients

Objective: Add custom headers and modify responses.

Scenario: Add a X-Service-Version header to all responses and a X-Request-ID for tracing.

Exercise 3: Implementation

curl http://127.0.0.1:9180/apisix/admin/routes/1 \
  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
  -X PUT -d '{
    "uris": ["/api/patients", "/api/patients/*"],
    "plugins": {
      "response-rewrite": {
        "headers": {
          "X-Service-Version": "v1.0",
          "X-Processed-By": "APISIX-Gateway"
        }
      },
      "request-id": {
        "header_name": "X-Request-ID",
        "include_in_response": true
      }
    },
    "upstream": {
      "type": "roundrobin",
      "nodes": {"practicemanager:8080": 1}
    }
  }'

Exercise 3: Testing

curl -i http://localhost:9080/api/patients/P001

Expected Headers:

X-Service-Version: v1.0
X-Processed-By: APISIX-Gateway
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

Discussion: How could you use X-Request-ID for distributed tracing?

Exercise 4: Load Balancing

Distributing Traffic Across Instances

Objective: Configure round-robin load balancing across multiple instances.

Setup: Start two instances of practicemanager on different ports:

# Terminal 1
cd practicemanager && ./mvnw quarkus:dev

# Terminal 2
cd practicemanager && ./mvnw quarkus:dev -Dquarkus.http.port=8081

Exercise 4: Implementation

curl http://127.0.0.1:9180/apisix/admin/routes/1 \
  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
  -X PUT -d '{
    "uris": ["/api/patients", "/api/patients/*"],
    "upstream": {
      "type": "roundrobin",
      "nodes": {
        "practicemanager:8080": 1,
        "practicemanager:8081": 1
      }
    }
  }'

Exercise 4: Testing

Add different responses to each instance to verify routing:

Modify each instance to log which port handles the request.

# Make 10 requests
for i in {1..10}; do
  curl http://localhost:9080/api/patients/P001
done

Check logs: You should see requests distributed between 8080 and 8081.

Try other algorithms: weighted round-robin, consistent hashing, least connections.

Exercise 5: Health Checks & Circuit Breaking

Automatic Failover

Objective: Configure health checks to automatically remove unhealthy nodes.

Exercise 5: Implementation

curl http://127.0.0.1:9180/apisix/admin/routes/1 \
  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
  -X PUT -d '{
    "uris": ["/api/patients", "/api/patients/*"],
    "upstream": {
      "type": "roundrobin",
      "nodes": {
        "practicemanager:8080": 1,
        "practicemanager:8081": 1
      },
      "checks": {
        "active": {
          "http_path": "/q/health/live",
          "healthy": {
            "interval": 2,
            "successes": 1
          },
          "unhealthy": {
            "interval": 1,
            "http_failures": 2
          }
        }
      }
    }
  }'

Exercise 5: Testing

Step 1: Verify both instances are healthy

curl http://localhost:9080/api/patients/P001

Step 2: Stop one instance (Ctrl+C on Terminal 2)

Step 3: Make requests - they should all succeed via port 8080

Step 4: Restart the stopped instance

Step 5: Verify it's added back to the pool automatically

Discussion: What happens during the health check interval?

Exercise 6: API Gateway as BFF

Backend for Frontend Pattern

Objective: Create separate API endpoints for mobile and web clients.

Scenario:

  • Mobile app needs simplified API paths
  • Web needs full backend API access

Exercise 6: BFF Pattern - The Right Way

Reality Check: Complex response transformations (like field filtering) in API Gateways are difficult and not recommended for production.

The Production Approach:

Create a mobile-specific route that signals client type to backend:

curl http://127.0.0.1:9180/apisix/admin/routes/10 \
  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
  -X PUT -d '{
    "uris": ["/mobile/api/patients/*"],
    "plugins": {
      "proxy-rewrite": {
        "regex_uri": ["^/mobile/api/(.*)", "/api/$1"],
        "headers": {
          "set": {
            "X-Client-Type": "mobile",
            "X-Response-Format": "minimal"
          }
        }
      }
    },
    "upstream": {
      "type": "roundrobin",
      "nodes": {"practicemanager:8080": 1}
    }
  }'

Backend Implementation:

The practicemanager service would read the X-Response-Format header and return only requested fields.

Why this approach?

  • Gateway handles routing and headers
  • Backend handles business logic (field selection)
  • Separation of concerns
  • Easier to test and maintain

Exercise 6: Testing BFF Routes

Test mobile endpoint:

# Mobile route - sends X-Client-Type header
curl -v http://localhost:9080/mobile/api/patients/P001

# Direct route - no special headers
curl -v http://localhost:9080/api/patients/P001

Verify headers are sent to backend:
Check practicemanager logs to see X-Client-Type: mobile header.

Current behavior: Both return full data (backend doesn't filter yet).

To implement filtering: Update practicemanager to check X-Response-Format header and return minimal fields.

Exercise 6: BFF Architecture Options

Option 1: Gateway + Backend Filtering (Recommended)

  • Gateway adds client-type headers
  • Backend service filters fields based on headers
  • Clean separation of concerns

Option 2: Dedicated BFF Microservice (Best for complex cases)

  • Separate service for each client type (mobile-bff, web-bff)
  • Aggregates data from multiple services
  • Complex transformations and composition

Option 3: Gateway-based Lua Transformation (Not Recommended)

  • Complex and hard to maintain
  • Limited debugging capabilities
  • Performance concerns

This course demonstrates Option 1 - the pragmatic approach.

Exercise 7: Observability

Monitoring Gateway Metrics

Objective: Enable Prometheus metrics and access logs.

curl http://127.0.0.1:9180/apisix/admin/global_rules/1 \
  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
  -X PUT -d '{
    "plugins": {
      "prometheus": {}
    }
  }'

Access metrics:

curl http://localhost:9091/apisix/prometheus/metrics

Need to add the 9091 port mapping in docker-compose for APISIX service.

Key Metrics to Monitor

  • apisix_http_status: HTTP status code distribution
  • apisix_bandwidth: Bytes in/out
  • apisix_http_latency: Request latency percentiles
  • apisix_upstream_status: Upstream health

Exercise: Connect these metrics to Grafana (covered in Observability module).

Discussion: Gateway Trade-offs

Benefits:

  • Single entry point
  • Centralized security, rate limiting, logging
  • Protocol translation
  • Reduced client complexity

Challenges:

  • Single point of failure (mitigate with HA setup)
  • Can become a bottleneck (scale horizontally)
  • Risk of "smart gateway, dumb services" anti-pattern

Best Practices

  1. Keep it Thin: Gateway should route and enforce policies, not contain business logic.
  2. Automate Configuration: Use GitOps or service discovery.
  3. Monitor Closely: Gateway failures affect all services.
  4. Version Your APIs: Use path or header versioning (/v1/patients).
  5. Test Failover: Regularly test circuit breakers and health checks.

Beyond Routing: Advanced Use Cases

API Composition
Aggregate multiple backend calls into one response (GraphQL-style).

Canary Deployments
Route 5% of traffic to new version, 95% to old.

A/B Testing
Route users based on headers/cookies to different backends.

Mock Responses
Return mock data for frontend development before backend is ready.

Summary

  • API Gateways are the front door of microservices architectures
  • They handle cross-cutting concerns (auth, rate limiting, SSL)
  • Apache APISIX provides dynamic, plugin-based routing
  • Gateways enable BFF pattern for different client needs
  • Always monitor and test failover scenarios

Next: We'll connect gateway logs to observability tools (Prometheus, Grafana, Jaeger).