Lab - Implementing API Gateway with Spring Cloud Gateway

Objective

Integrate Spring Cloud Gateway into our microservices system from the previous module and implement circuit breaker patterns to improve resilience.

Prerequisites

  • Completed Module 7 lab with the following microservices:
    • Book Service (port 8081)
    • User Service (port 8082)
    • Analytics Service (port 8083)
    • Eureka Server (port 8761)

Step 1: Create the API Gateway Project

  1. Create a new Spring Boot project:
  • Group: com.example
  • Artifact: api-gateway
  • Dependencies:
    • Spring Cloud Gateway
    • Eureka Discovery Client
    • Spring Boot Actuator
    • Spring Cloud Circuit Breaker Resilience4j
  1. Configure the application.yml file:
server:
  port: 8080

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
        - id: book-service
          uri: lb://book-service
          predicates:
            - Path=/api/books/**
          filters:
            - name: CircuitBreaker
              args:
                name: bookService
                fallbackUri: forward:/fallback/books

        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/users/**
          filters:
            - name: CircuitBreaker
              args:
                name: userService
                fallbackUri: forward:/fallback/users

        - id: analytics-service
          uri: lb://analytics-service
          predicates:
            - Path=/api/analytics/**
          filters:
            - name: CircuitBreaker
              args:
                name: analyticsService
                fallbackUri: forward:/fallback/analytics

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
  instance:
    preferIpAddress: true

management:
  endpoints:
    web:
      exposure:
        include: gateway,health,info
  endpoint:
    gateway:
      enabled: true
    health:
      show-details: always

Step 2: Implement Fallback Controller

Create a fallback controller to handle circuit-breaker fallbacks:

package com.example.apigateway.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/fallback")
public class FallbackController {

    @GetMapping("/books")
    public Mono<Map<String, String>> bookServiceFallback() {
        Map<String, String> response = new HashMap<>();
        response.put("status", "error");
        response.put("message", "Book Service is currently unavailable. Please try again later.");
        return Mono.just(response);
    }

    @GetMapping("/users")
    public Mono<Map<String, String>> userServiceFallback() {
        Map<String, String> response = new HashMap<>();
        response.put("status", "error");
        response.put("message", "User Service is currently unavailable. Please try again later.");
        return Mono.just(response);
    }

    @GetMapping("/analytics")
    public Mono<Map<String, String>> analyticsServiceFallback() {
        Map<String, String> response = new HashMap<>();
        response.put("status", "error");
        response.put("message", "Analytics Service is currently unavailable. Please try again later.");
        return Mono.just(response);
    }
}

Step 3: Configure Circuit Breaker

Create a configuration class for Resilience4j circuit breaker:

package com.example.apigateway.config;

import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

@Configuration
public class CircuitBreakerConfiguration {

    @Bean
    public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer() {
        return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
                .circuitBreakerConfig(CircuitBreakerConfig.custom()
                        .slidingWindowSize(10)
                        .failureRateThreshold(50)
                        .waitDurationInOpenState(Duration.ofSeconds(10))
                        .permittedNumberOfCallsInHalfOpenState(5)
                        .slowCallRateThreshold(50)
                        .slowCallDurationThreshold(Duration.ofSeconds(2))
                        .build())
                .timeLimiterConfig(TimeLimiterConfig.custom()
                        .timeoutDuration(Duration.ofSeconds(3))
                        .build())
                .build());
    }
}

Step 4: Add Rate Limiting

Implement a request rate limiter using Redis:

  1. Add Redis and Redis Rate Limiter dependencies to pom.xml:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway-mvc</artifactId>
</dependency>
  1. Configure Redis rate limiter in application.yml:
spring:
  redis:
    host: localhost
    port: 6379
  cloud:
    gateway:
      routes:
        - id: book-service
          uri: lb://book-service
          predicates:
            - Path=/api/books/**
          filters:
            - name: CircuitBreaker
              args:
                name: bookService
                fallbackUri: forward:/fallback/books
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20

Step 5: Create Custom Filter for Request Logging

Create a global filter for logging:

package com.example.apigateway.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.UUID;

@Component
public class LoggingFilter implements GlobalFilter, Ordered {

    private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();

        String requestId = UUID.randomUUID().toString();
        logger.info("Request ID: {}, Method: {}, Path: {}, Source IP: {}",
                requestId,
                request.getMethod(),
                request.getPath(),
                request.getRemoteAddress());

        long startTime = System.currentTimeMillis();

        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            long endTime = System.currentTimeMillis();
            logger.info("Request ID: {}, Response Status: {}, Duration: {} ms",
                    requestId,
                    exchange.getResponse().getStatusCode(),
                    (endTime - startTime));
        }));
    }

    @Override
    public int getOrder() {
        return -1; // High priority to ensure this runs first
    }
}

Step 6: Implement CORS Configuration

Add CORS support to allow frontend applications to access our API:

package com.example.apigateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

@Configuration
public class CorsConfig {

    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration corsConfig = new CorsConfiguration();
        corsConfig.setAllowedOrigins(Arrays.asList("*"));
        corsConfig.setMaxAge(3600L);
        corsConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        corsConfig.setAllowedHeaders(Arrays.asList("*"));

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfig);

        return new CorsWebFilter(source);
    }
}

Step 7: Implement Custom Route for API Documentation

Create a custom route to aggregate Swagger documentation from all services:

  1. Add the OpenAPI dependency:
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-webflux-ui</artifactId>
    <version>1.6.15</version>
</dependency>
  1. Configure in application.yml:
spring:
  cloud:
    gateway:
      routes:
        # Add these routes for API documentation
        - id: book-service-docs
          uri: lb://book-service
          predicates:
            - Path=/book-service/v3/api-docs/**
          filters:
            - RewritePath=/book-service/(?<segment>.*), /$\{segment}

        - id: user-service-docs
          uri: lb://user-service
          predicates:
            - Path=/user-service/v3/api-docs/**
          filters:
            - RewritePath=/user-service/(?<segment>.*), /$\{segment}

        - id: analytics-service-docs
          uri: lb://analytics-service
          predicates:
            - Path=/analytics-service/v3/api-docs/**
          filters:
            - RewritePath=/analytics-service/(?<segment>.*), /$\{segment}
  1. Create a controller for the OpenAPI documentation:
package com.example.apigateway.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
public class ApiDocsController {

    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("/api-docs")
    public Mono<Map<String, Object>> getApiDocs() {
        List<String> services = discoveryClient.getServices();
        List<Map<String, String>> serviceUrls = new ArrayList<>();

        for (String service : services) {
            if (!service.equals("api-gateway")) {
                Map<String, String> serviceUrl = new HashMap<>();
                serviceUrl.put("name", service);
                serviceUrl.put("url", "/" + service + "/v3/api-docs");
                serviceUrls.add(serviceUrl);
            }
        }

        Map<String, Object> response = new HashMap<>();
        response.put("services", serviceUrls);
        return Mono.just(response);
    }
}

Step 8: Testing the API Gateway

  1. Start all the microservices from Module 7:
  • Eureka Server
  • Book Service
  • User Service
  • Analytics Service
  1. Start the API Gateway application

  2. Test the Gateway using curl or Postman:

# Create a user through the gateway
curl -X POST http://localhost:8080/api/users -H "Content-Type: application/json" -d '{
  "username": "john_doe",
  "email": "[email protected]",
  "password": "password123"
}'

# Create a book
curl -X POST http://localhost:8080/api/books -H "Content-Type: application/json" -d '{
  "title": "Spring Cloud in Action",
  "author": "John Smith",
  "isbn": "9781617295423",
  "price": 39.99,
  "userId": 1
}'

# Get all books
curl http://localhost:8080/api/books

# Get analytics
curl http://localhost:8080/api/analytics/summary

# Check Eureka dashboard
# Open browser at http://localhost:8761

# Check gateway routes
curl http://localhost:8080/actuator/gateway/routes
  1. Test the Circuit Breaker:
  • Stop the book service

  • Try to access books through the gateway:

    curl http://localhost:8080/api/books
  • You should receive the fallback response

Step 9: Enhance API Gateway with Request Transformation

Add a filter to transform requests:

package com.example.apigateway.filter;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

@Component
public class AddRequestHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory<AddRequestHeaderGatewayFilterFactory.Config> {

    public AddRequestHeaderGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest().mutate()
                    .header(config.getHeaderName(), config.getHeaderValue())
                    .build();

            ServerWebExchange mutatedExchange = exchange.mutate().request(request).build();

            return chain.filter(mutatedExchange);
        };
    }

    public static class Config {
        private String headerName;
        private String headerValue;

        public String getHeaderName() {
            return headerName;
        }

        public void setHeaderName(String headerName) {
            this.headerName = headerName;
        }

        public String getHeaderValue() {
            return headerValue;
        }

        public void setHeaderValue(String headerValue) {
            this.headerValue = headerValue;
        }
    }
}

Update the routes in application.yml:

spring:
  cloud:
    gateway:
      routes:
        - id: book-service
          uri: lb://book-service
          predicates:
            - Path=/api/books/**
          filters:
            - name: CircuitBreaker
              args:
                name: bookService
                fallbackUri: forward:/fallback/books
            - AddRequestHeader=X-Source, api-gateway

Step 10: Implement a Route-specific Metrics Filter

Create a custom filter for collecting metrics:

package com.example.apigateway.filter;

import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.time.Instant;

@Component
public class MetricsFilter implements GlobalFilter, Ordered {

    private final MeterRegistry meterRegistry;

    public MetricsFilter(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        Instant start = Instant.now();
        String path = exchange.getRequest().getPath().value();
        String method = exchange.getRequest().getMethod().name();

        return chain.filter(exchange)
                .doFinally(signalType -> {
                    Duration duration = Duration.between(start, Instant.now());
                    String outcome = exchange.getResponse().getStatusCode() != null ?
                                    exchange.getResponse().getStatusCode().toString() : "Unknown";

                    meterRegistry.timer("gateway.requests",
                                        "path", path,
                                        "method", method,
                                        "outcome", outcome)
                                .record(duration);

                    meterRegistry.counter("gateway.requests.count",
                                        "path", path,
                                        "method", method,
                                        "outcome", outcome)
                                .increment();
                });
    }

    @Override
    public int getOrder() {
        return -2; // Run before LoggingFilter but after security filters
    }
}

Step 11: Integration with the Microservices

  1. Update the user-service to add a response header indicating the instance it’s running on:
@RestController
@RequestMapping("/api/users")
public class UserController {

    @Value("${eureka.instance.instance-id:${random.uuid}}")
    private String instanceId;

    // Other methods and dependencies...

    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> users = userService.getAllUsers();
        return ResponseEntity.ok()
                .header("X-Instance-Id", instanceId)
                .body(users);
    }
}
  1. Update the book-service similarly to add instance identification.

Step 12: Deploy and Test the Complete System

  1. Start all services:
  • Eureka Server
  • Config Server (if you have one)
  • Book Service
  • User Service
  • Analytics Service
  • API Gateway
  1. Use the API Gateway to access all microservices:
# Access user service through gateway
curl http://localhost:8080/api/users

# Access book service through gateway
curl http://localhost:8080/api/books

# Access analytics service through gateway
curl http://localhost:8080/api/analytics/summary

# Access the API documentation
curl http://localhost:8080/api-docs
  1. Test scaling by starting multiple instances of a service:

Start another instance of the book service on a different port:

java -jar book-service/target/book-service-0.0.1-SNAPSHOT.jar --server.port=8091

Watch the load balancing in action by making multiple requests:

for i in {1..10}; do curl http://localhost:8080/api/books; done

Observe the X-Instance-Id header to confirm requests are being distributed.

Summary

In this module, we successfully:

  1. Created a Spring Cloud Gateway as an entry point to our microservices system
  2. Implemented circuit breakers to handle service failures gracefully
  3. Added rate limiting to protect our services from overload
  4. Created custom filters for logging, metrics, and request transformation
  5. Configured CORS to allow frontend applications to access our API
  6. Set up API documentation aggregation to provide a unified view of all service APIs
  7. Tested load balancing by scaling individual services

Key benefits of our Gateway implementation:

  • Simplified client access: Clients only need to know about one endpoint
  • Enhanced resilience: Circuit breakers prevent cascading failures
  • Improved security: Centralized authentication and rate limiting
  • Better monitoring: Centralized request logging and metrics
  • Simplified API exploration: Aggregated API documentation

In the next module, we’ll focus on containerizing our microservices system using Docker, making it more portable and easier to deploy in various environments.


By Wahid Hamdi