Skip to content

5.3. API Gateway Usage

Core Functions

Routing

  • Path-based routing
  • Service discovery
  • Load balancing
  • Circuit breaking

Security

  • Authentication
  • Authorization
  • SSL/TLS termination
  • API key management

Monitoring

  • Request/response logging
  • Metrics collection
  • Performance monitoring
  • Error tracking

Kong Gateway

Features

  • Plugin architecture
  • Declarative configuration
  • Database-less mode
  • Kubernetes integration

Plugins

  • Rate limiting
  • JWT authentication
  • OAuth2
  • Request transformer

Deployment

  • Docker containers
  • Kubernetes operator
  • Hybrid mode
  • Multi-datacenter

Apigee

Features

  • API lifecycle management
  • Developer portal
  • Analytics dashboard
  • Monetization

Security

  • OAuth2/OpenID Connect
  • API key management
  • Threat protection
  • Data encryption

Integration

  • Cloud services
  • On-premise systems
  • Third-party APIs
  • Custom backends

Spring Boot API Gateway Implementation

Spring Cloud Gateway Configuration

java
@Configuration
@EnableConfigurationProperties({GatewayProperties.class})
public class ApiGatewayConfig {
    
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            // User Service Routes
            .route("user-service", r -> r
                .path("/api/users/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .addRequestHeader("X-Gateway", "spring-cloud-gateway")
                    .circuitBreaker(config -> config
                        .setName("user-service-cb")
                        .setFallbackUri("forward:/fallback/users"))
                    .retry(config -> config
                        .setRetries(3)
                        .setBackoff(Duration.ofMillis(100), Duration.ofSeconds(1), 2, true)))
                .uri("lb://user-service"))
                
            // Order Service Routes
            .route("order-service", r -> r
                .path("/api/orders/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .addRequestHeader("X-Gateway", "spring-cloud-gateway")
                    .requestRateLimiter(config -> config
                        .setRateLimiter(redisRateLimiter())
                        .setKeyResolver(userKeyResolver()))
                    .circuitBreaker(config -> config
                        .setName("order-service-cb")
                        .setFallbackUri("forward:/fallback/orders")))
                .uri("lb://order-service"))
                
            // Payment Service Routes
            .route("payment-service", r -> r
                .path("/api/payments/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .addRequestHeader("X-Gateway", "spring-cloud-gateway")
                    .filter(new AuthenticationGatewayFilterFactory().apply(
                        new AuthenticationGatewayFilterFactory.Config()))
                    .requestRateLimiter(config -> config
                        .setRateLimiter(redisRateLimiter())
                        .setKeyResolver(apiKeyResolver())))
                .uri("lb://payment-service"))
                
            // Notification Service Routes
            .route("notification-service", r -> r
                .path("/api/notifications/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .addRequestHeader("X-Gateway", "spring-cloud-gateway"))
                .uri("lb://notification-service"))
                
            build();
    }
    
    @Bean
    public RedisRateLimiter redisRateLimiter() {
        return new RedisRateLimiter(100, 200, 1); // replenishRate, burstCapacity, requestedTokens
    }
    
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> {
            String userId = exchange.getRequest().getHeaders().getFirst("X-User-ID");
            return Mono.just(userId != null ? "user:" + userId : "anonymous");
        };
    }
    
    @Bean
    public KeyResolver apiKeyResolver() {
        return exchange -> {
            String apiKey = exchange.getRequest().getHeaders().getFirst("X-API-Key");
            return Mono.just(apiKey != null ? "api:" + apiKey : "anonymous");
        };
    }
    
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> {
            String clientIp = getClientIpAddress(exchange.getRequest());
            return Mono.just("ip:" + clientIp);
        };
    }
    
    private String getClientIpAddress(ServerHttpRequest request) {
        String xForwardedFor = request.getHeaders().getFirst("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            return xForwardedFor.split(",")[0].trim();
        }
        
        String xRealIp = request.getHeaders().getFirst("X-Real-IP");
        if (xRealIp != null && !xRealIp.isEmpty()) {
            return xRealIp;
        }
        
        return request.getRemoteAddress() != null ? 
            request.getRemoteAddress().getAddress().getHostAddress() : "unknown";
    }
}

Custom Authentication Filter

java
@Component
public class AuthenticationGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthenticationGatewayFilterFactory.Config> {
    
    @Autowired
    private JwtTokenValidator jwtTokenValidator;
    
    @Autowired
    private ApiKeyValidator apiKeyValidator;
    
    public AuthenticationGatewayFilterFactory() {
        super(Config.class);
    }
    
    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            
            // Skip authentication for health checks
            if (request.getPath().value().contains("/actuator/health")) {
                return chain.filter(exchange);
            }
            
            // Try JWT authentication first
            String authHeader = request.getHeaders().getFirst("Authorization");
            if (authHeader != null && authHeader.startsWith("Bearer ")) {
                String token = authHeader.substring(7);
                return jwtTokenValidator.validateToken(token)
                    .flatMap(claims -> {
                        ServerHttpRequest mutatedRequest = request.mutate()
                            .header("X-User-ID", claims.getSubject())
                            .header("X-User-Roles", String.join(",", claims.getRoles()))
                            .build();
                        return chain.filter(exchange.mutate().request(mutatedRequest).build());
                    })
                    .onErrorResume(error -> {
                        log.warn("JWT validation failed: {}", error.getMessage());
                        return handleAuthenticationError(exchange, "Invalid JWT token");
                    });
            }
            
            // Try API Key authentication
            String apiKey = request.getHeaders().getFirst("X-API-Key");
            if (apiKey != null) {
                return apiKeyValidator.validateApiKey(apiKey)
                    .flatMap(apiKeyInfo -> {
                        ServerHttpRequest mutatedRequest = request.mutate()
                            .header("X-Client-ID", apiKeyInfo.getClientId())
                            .header("X-API-Key-Scope", String.join(",", apiKeyInfo.getScopes()))
                            .build();
                        return chain.filter(exchange.mutate().request(mutatedRequest).build());
                    })
                    .onErrorResume(error -> {
                        log.warn("API Key validation failed: {}", error.getMessage());
                        return handleAuthenticationError(exchange, "Invalid API key");
                    });
            }
            
            // No authentication provided
            return handleAuthenticationError(exchange, "Authentication required");
        };
    }
    
    private Mono<Void> handleAuthenticationError(ServerWebExchange exchange, String message) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().add("Content-Type", "application/json");
        
        String errorResponse = """
            {
                "error": "Unauthorized",
                "message": "%s",
                "timestamp": "%s"
            }
            """.formatted(message, Instant.now().toString());
        
        DataBuffer buffer = response.bufferFactory().wrap(errorResponse.getBytes());
        return response.writeWith(Mono.just(buffer));
    }
    
    @Data
    public static class Config {
        private boolean required = true;
        private List<String> excludePaths = new ArrayList<>();
    }
}

@Component
@Slf4j
public class JwtTokenValidator {
    
    @Value("${jwt.secret}")
    private String jwtSecret;
    
    @Value("${jwt.expiration:3600}")
    private long jwtExpiration;
    
    private Key getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
        return Keys.hmacShaKeyFor(keyBytes);
    }
    
    public Mono<JwtClaims> validateToken(String token) {
        return Mono.fromCallable(() -> {
            Claims claims = Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
            
            List<String> roles = claims.get("roles", List.class);
            return new JwtClaims(claims.getSubject(), roles != null ? roles : new ArrayList<>());
        })
        .doOnError(error -> log.error("JWT validation error: {}", error.getMessage()))
        .onErrorMap(JwtException.class, ex -> new AuthenticationException("Invalid JWT token: " + ex.getMessage()));
    }
}

@Data
@AllArgsConstructor
public class JwtClaims {
    private String subject;
    private List<String> roles;
}

@Component
@Slf4j
public class ApiKeyValidator {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private ApiKeyRepository apiKeyRepository;
    
    public Mono<ApiKeyInfo> validateApiKey(String apiKey) {
        return Mono.fromCallable(() -> {
            // Try cache first
            String cacheKey = "api_key:" + apiKey;
            ApiKeyInfo cachedInfo = (ApiKeyInfo) redisTemplate.opsForValue().get(cacheKey);
            
            if (cachedInfo != null) {
                if (cachedInfo.isExpired()) {
                    redisTemplate.delete(cacheKey);
                    throw new AuthenticationException("API key expired");
                }
                return cachedInfo;
            }
            
            // Fallback to database
            ApiKeyEntity entity = apiKeyRepository.findByKeyHash(hashApiKey(apiKey))
                .orElseThrow(() -> new AuthenticationException("Invalid API key"));
            
            if (entity.isExpired() || !entity.isActive()) {
                throw new AuthenticationException("API key expired or inactive");
            }
            
            ApiKeyInfo info = new ApiKeyInfo(
                entity.getClientId(),
                entity.getScopes(),
                entity.getExpiresAt(),
                entity.getRateLimit()
            );
            
            // Cache for 5 minutes
            redisTemplate.opsForValue().set(cacheKey, info, Duration.ofMinutes(5));
            
            return info;
        })
        .doOnError(error -> log.error("API key validation error: {}", error.getMessage()));
    }
    
    private String hashApiKey(String apiKey) {
        return DigestUtils.sha256Hex(apiKey);
    }
}

@Data
@AllArgsConstructor
public class ApiKeyInfo implements Serializable {
    private String clientId;
    private List<String> scopes;
    private Instant expiresAt;
    private Integer rateLimit;
    
    public boolean isExpired() {
        return expiresAt != null && Instant.now().isAfter(expiresAt);
    }
}

public class AuthenticationException extends RuntimeException {
    public AuthenticationException(String message) {
        super(message);
    }
}

Circuit Breaker Configuration

java
@Configuration
public class CircuitBreakerConfig {
    
    @Bean
    public Customizer<ReactiveResilience4JCircuitBreakerFactory> circuitBreakerCustomizer() {
        return factory -> {
            factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
                .circuitBreakerConfig(CircuitBreakerConfiguration.ofDefaults())
                .timeLimiterConfig(TimeLimiterConfiguration.ofDefaults())
                .build());
            
            // User service specific configuration
            factory.configure(builder -> builder
                .circuitBreakerConfig(CircuitBreakerConfiguration.custom()
                    .failureRateThreshold(50)
                    .waitDurationInOpenState(Duration.ofSeconds(30))
                    .slidingWindowSize(10)
                    .minimumNumberOfCalls(5)
                    .build())
                .timeLimiterConfig(TimeLimiterConfiguration.custom()
                    .timeoutDuration(Duration.ofSeconds(5))
                    .build()), "user-service-cb");
                    
            // Order service specific configuration
            factory.configure(builder -> builder
                .circuitBreakerConfig(CircuitBreakerConfiguration.custom()
                    .failureRateThreshold(60)
                    .waitDurationInOpenState(Duration.ofSeconds(45))
                    .slidingWindowSize(15)
                    .minimumNumberOfCalls(8)
                    .build())
                .timeLimiterConfig(TimeLimiterConfiguration.custom()
                    .timeoutDuration(Duration.ofSeconds(10))
                    .build()), "order-service-cb");
        };
    }
}

Fallback Controllers

java
@RestController
@RequestMapping("/fallback")
@Slf4j
public class FallbackController {
    
    @GetMapping("/users/**")
    public ResponseEntity<Map<String, Object>> userServiceFallback(HttpServletRequest request) {
        log.warn("User service fallback triggered for request: {}", request.getRequestURI());
        
        Map<String, Object> fallbackResponse = Map.of(
            "error", "Service Temporarily Unavailable",
            "message", "User service is currently experiencing issues. Please try again later.",
            "service", "user-service",
            "timestamp", Instant.now(),
            "fallback", true
        );
        
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(fallbackResponse);
    }
    
    @GetMapping("/orders/**")
    public ResponseEntity<Map<String, Object>> orderServiceFallback(HttpServletRequest request) {
        log.warn("Order service fallback triggered for request: {}", request.getRequestURI());
        
        Map<String, Object> fallbackResponse = Map.of(
            "error", "Service Temporarily Unavailable",
            "message", "Order service is currently experiencing issues. Please try again later.",
            "service", "order-service",
            "timestamp", Instant.now(),
            "fallback", true
        );
        
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(fallbackResponse);
    }
    
    @PostMapping("/orders/**")
    public ResponseEntity<Map<String, Object>> orderServicePostFallback(HttpServletRequest request) {
        log.warn("Order service POST fallback triggered for request: {}", request.getRequestURI());
        
        Map<String, Object> fallbackResponse = Map.of(
            "error", "Service Temporarily Unavailable",
            "message", "Order creation is currently unavailable. Your request has been queued and will be processed when the service recovers.",
            "service", "order-service",
            "timestamp", Instant.now(),
            "fallback", true,
            "queuedForProcessing", true
        );
        
        return ResponseEntity.status(HttpStatus.ACCEPTED).body(fallbackResponse);
    }
}

Gateway Metrics and Monitoring

java
@Component
@Slf4j
public class GatewayMetricsFilter implements GlobalFilter, Ordered {
    
    private final MeterRegistry meterRegistry;
    private final Timer.Sample requestTimer;
    
    public GatewayMetricsFilter(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.requestTimer = Timer.start(meterRegistry);
    }
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        Timer.Sample sample = Timer.start(meterRegistry);
        String route = getRouteName(exchange);
        String method = exchange.getRequest().getMethod().name();
        
        return chain.filter(exchange)
            .doOnSuccess(aVoid -> recordMetrics(sample, route, method, exchange, true))
            .doOnError(error -> recordMetrics(sample, route, method, exchange, false))
            .doFinally(signalType -> {
                // Record additional metrics
                recordRequestMetrics(exchange, route, method);
            });
    }
    
    private void recordMetrics(Timer.Sample sample, String route, String method, 
                             ServerWebExchange exchange, boolean success) {
        HttpStatus status = exchange.getResponse().getStatusCode();
        
        sample.stop(Timer.builder("gateway.request.duration")
            .tag("route", route)
            .tag("method", method)
            .tag("status", status != null ? status.toString() : "unknown")
            .tag("success", String.valueOf(success))
            .register(meterRegistry));
    }
    
    private void recordRequestMetrics(ServerWebExchange exchange, String route, String method) {
        Counter.builder("gateway.requests.total")
            .tag("route", route)
            .tag("method", method)
            .register(meterRegistry)
            .increment();
            
        // Record response size
        ServerHttpResponse response = exchange.getResponse();
        if (response.getHeaders().getContentLength() > 0) {
            DistributionSummary.builder("gateway.response.size")
                .tag("route", route)
                .tag("method", method)
                .register(meterRegistry)
                .record(response.getHeaders().getContentLength());
        }
    }
    
    private String getRouteName(ServerWebExchange exchange) {
        Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        return route != null ? route.getId() : "unknown";
    }
    
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

@Component
@Slf4j
public class GatewayLoggingFilter implements GlobalFilter, Ordered {
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        
        String requestId = UUID.randomUUID().toString();
        String clientIp = getClientIpAddress(request);
        String userAgent = request.getHeaders().getFirst("User-Agent");
        
        // Add request ID to headers for tracing
        ServerHttpRequest mutatedRequest = request.mutate()
            .header("X-Request-ID", requestId)
            .build();
        
        long startTime = System.currentTimeMillis();
        
        log.info("Gateway Request - ID: {}, Method: {}, URI: {}, IP: {}, User-Agent: {}",
            requestId, request.getMethod(), request.getURI(), clientIp, userAgent);
        
        return chain.filter(exchange.mutate().request(mutatedRequest).build())
            .doFinally(signalType -> {
                long duration = System.currentTimeMillis() - startTime;
                HttpStatus status = exchange.getResponse().getStatusCode();
                
                log.info("Gateway Response - ID: {}, Status: {}, Duration: {}ms",
                    requestId, status, duration);
            });
    }
    
    private String getClientIpAddress(ServerHttpRequest request) {
        String xForwardedFor = request.getHeaders().getFirst("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            return xForwardedFor.split(",")[0].trim();
        }
        
        String xRealIp = request.getHeaders().getFirst("X-Real-IP");
        if (xRealIp != null && !xRealIp.isEmpty()) {
            return xRealIp;
        }
        
        return request.getRemoteAddress() != null ? 
            request.getRemoteAddress().getAddress().getHostAddress() : "unknown";
    }
    
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1;
    }
}

API Gateway Health Checks

java
@Component
public class ServiceHealthIndicator implements HealthIndicator {
    
    @Autowired
    private ReactiveLoadBalancerClientFactory loadBalancerClientFactory;
    
    @Autowired
    private WebClient.Builder webClientBuilder;
    
    private final List<String> serviceNames = Arrays.asList(
        "user-service", "order-service", "payment-service", "notification-service"
    );
    
    @Override
    public Health health() {
        Health.Builder builder = Health.up();
        Map<String, Object> details = new HashMap<>();
        
        for (String serviceName : serviceNames) {
            try {
                ServiceHealthStatus status = checkServiceHealth(serviceName);
                details.put(serviceName, status);
                
                if (!status.isHealthy()) {
                    builder.down();
                }
            } catch (Exception e) {
                details.put(serviceName, new ServiceHealthStatus(false, e.getMessage()));
                builder.down();
            }
        }
        
        return builder.withDetails(details).build();
    }
    
    private ServiceHealthStatus checkServiceHealth(String serviceName) {
        try {
            ReactiveLoadBalancerClient loadBalancerClient = 
                loadBalancerClientFactory.getInstance(serviceName);
            
            WebClient webClient = webClientBuilder.build();
            
            String healthResponse = webClient.get()
                .uri("lb://" + serviceName + "/actuator/health")
                .retrieve()
                .bodyToMono(String.class)
                .timeout(Duration.ofSeconds(5))
                .block();
                
            return new ServiceHealthStatus(true, "Service is healthy");
        } catch (Exception e) {
            log.warn("Health check failed for service {}: {}", serviceName, e.getMessage());
            return new ServiceHealthStatus(false, e.getMessage());
        }
    }
    
    @Data
    @AllArgsConstructor
    public static class ServiceHealthStatus {
        private boolean healthy;
        private String message;
    }
}

This implementation provides comprehensive Spring Cloud Gateway usage in production environments with security and scalability considerations.

Created by Eren Demir.