Part 48 of 59
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-nameresolves 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
RequestRateLimiterfilter — 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.