API Gateway with Spring Cloud Gateway

The API gateway is the single entry point for all client traffic. It handles routing to downstream services, authentication, rate limiting, and request/response transformation — so individual services don’t have to.

Setup

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

Spring Cloud Gateway is reactive (WebFlux-based) — don’t include spring-boot-starter-web.

Route Configuration

YAML Configuration

spring:
  application:
    name: api-gateway

  cloud:
    gateway:
      routes:

        - id: order-service
          uri: lb://order-service          # lb:// = load-balanced via Eureka
          predicates:
            - Path=/api/orders/**
          filters:
            - StripPrefix=0               # don't strip path prefix
            - AddRequestHeader=X-Gateway-Source, api-gateway

        - id: customer-service
          uri: lb://customer-service
          predicates:
            - Path=/api/customers/**
          filters:
            - StripPrefix=0

        - id: payment-service
          uri: lb://payment-service
          predicates:
            - Path=/api/payments/**
          filters:
            - StripPrefix=0

        # Versioned routing
        - id: order-service-v2
          uri: lb://order-service-v2
          predicates:
            - Path=/api/v2/orders/**
            - Header=X-API-Version, 2

      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin

Programmatic Route Configuration

@Configuration
public class GatewayRouteConfig {

    @Bean
    public RouteLocator routeLocator(RouteLocatorBuilder builder) {
        return builder.routes()

            .route("order-service", r -> r
                .path("/api/orders/**")
                .filters(f -> f
                    .addRequestHeader("X-Gateway-Source", "api-gateway")
                    .circuitBreaker(c -> c
                        .setName("order-cb")
                        .setFallbackUri("forward:/fallback/orders")))
                .uri("lb://order-service"))

            .route("customer-service", r -> r
                .path("/api/customers/**")
                .and()
                .header("Authorization")   // only route if Authorization header present
                .uri("lb://customer-service"))

            .route("health", r -> r
                .path("/health")
                .filters(f -> f.setPath("/actuator/health"))
                .uri("lb://order-service"))

            .build();
    }

    @GetMapping("/fallback/orders")
    public ResponseEntity<String> ordersFallback() {
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
            .body("{\"message\": \"Order service temporarily unavailable\"}");
    }
}

JWT Authentication at the Gateway

Validate JWTs once at the gateway — downstream services trust the gateway-added headers:

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter implements GlobalFilter, Ordered {

    private final JwtValidator jwtValidator;

    private static final List<String> PUBLIC_PATHS = List.of(
        "/api/auth/login",
        "/api/auth/register",
        "/actuator/health"
    );

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

        if (PUBLIC_PATHS.stream().anyMatch(path::startsWith)) {
            return chain.filter(exchange);
        }

        String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        String token = authHeader.substring(7);

        return Mono.fromCallable(() -> jwtValidator.validate(token))
            .flatMap(claims -> {
                // Add user info to headers for downstream services
                ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
                    .header("X-User-Id", claims.getSubject())
                    .header("X-User-Email", claims.get("email", String.class))
                    .header("X-User-Roles", String.join(",", claims.get("roles", List.class)))
                    .build();

                return chain.filter(exchange.mutate().request(mutatedRequest).build());
            })
            .onErrorResume(JwtValidationException.class, e -> {
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            });
    }

    @Override
    public int getOrder() { return -1; }   // run before routing
}

Downstream services extract user info from headers:

@GetMapping("/api/orders")
public Page<OrderResponse> getOrders(
        @RequestHeader("X-User-Id") UUID userId,
        @RequestHeader("X-User-Roles") String roles,
        Pageable pageable) {
    return orderService.findByUser(userId, pageable);
}

Rate Limiting

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10    # 10 requests/second
                redis-rate-limiter.burstCapacity: 20    # burst to 20
                redis-rate-limiter.requestedTokens: 1
                key-resolver: "#{@userKeyResolver}"
@Bean
public KeyResolver userKeyResolver() {
    // Rate limit per user (from JWT) or per IP
    return exchange -> {
        String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
        if (userId != null) return Mono.just(userId);
        return Mono.just(
            Objects.requireNonNull(exchange.getRequest().getRemoteAddress())
                .getAddress().getHostAddress());
    };
}

Circuit Breaker

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
spring:
  cloud:
    gateway:
      routes:
        - id: payment-service
          uri: lb://payment-service
          predicates:
            - Path=/api/payments/**
          filters:
            - name: CircuitBreaker
              args:
                name: payment-cb
                fallbackUri: forward:/fallback/payment

resilience4j:
  circuitbreaker:
    instances:
      payment-cb:
        sliding-window-size: 10
        failure-rate-threshold: 50      # open after 50% failures
        wait-duration-in-open-state: 30s
        permitted-number-of-calls-in-half-open-state: 5

Request/Response Transformation

filters:
  # Rewrite path
  - RewritePath=/api/v1/(?<segment>.*), /api/$\{segment}

  # Add/remove headers
  - AddRequestHeader=X-Request-Source, gateway
  - RemoveRequestHeader=X-Internal-Token

  # Modify response
  - AddResponseHeader=X-Response-Time, $\{T(System).currentTimeMillis()}

  # Retry on failure
  - name: Retry
    args:
      retries: 3
      statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
      methods: GET
      backoff:
        firstBackoff: 50ms
        maxBackoff: 500ms
        factor: 2

CORS at the Gateway

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origins:
              - https://app.devopsmonk.com
              - https://admin.devopsmonk.com
            allowed-methods:
              - GET
              - POST
              - PUT
              - DELETE
              - OPTIONS
            allowed-headers: "*"
            allow-credentials: true
            max-age: 3600

Handle CORS once at the gateway — remove CORS config from individual services.

Request Logging

@Component
@Slf4j
public class RequestLoggingFilter implements GlobalFilter {

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

        long startTime = System.currentTimeMillis();

        return chain.filter(exchange.mutate()
                .request(request.mutate()
                    .header("X-Request-Id", requestId)
                    .build())
                .build())
            .doFinally(signalType -> {
                long elapsed = System.currentTimeMillis() - startTime;
                log.info("method={}, path={}, requestId={}, status={}, elapsed={}ms",
                    request.getMethod(),
                    request.getPath(),
                    requestId,
                    exchange.getResponse().getStatusCode(),
                    elapsed);
            });
    }
}

Actuator Integration

management:
  endpoint:
    gateway:
      enabled: true
  endpoints:
    web:
      exposure:
        include: gateway,health,info

# GET /actuator/gateway/routes — list all routes
# GET /actuator/gateway/routedefinitions — route definitions
# POST /actuator/gateway/refresh — reload routes without restart

What You’ve Learned

  • Spring Cloud Gateway routes requests to upstream services by path, header, method, or query parameter
  • lb://service-name resolves service instances from Eureka with client-side load balancing
  • A global JWT filter validates tokens once at the gateway and passes user info in headers to downstream services
  • Rate limiting with Redis and RequestRateLimiter filter — per-user or per-IP
  • Circuit breaker filter wraps downstream calls — returns fallback response when service is unavailable
  • Handle CORS and logging once at the gateway — keep individual services simpler

Next: Article 49 — Centralized Configuration with Spring Cloud Config Server — manage configuration for all services from one place.