Why API Gateway?
Without a gateway, clients need to know about every microservice: user-service on port 8081, order-service on 8082, payment-service on 8083... The gateway provides a single entry point that routes requests to the right service.
// WITHOUT Gateway - Client needs to know all services
fetch('http://user-service:8081/users/1')
fetch('http://order-service:8082/orders')
fetch('http://payment-service:8083/payments')
// WITH Gateway - One endpoint
fetch('http://api-gateway/users/1')
fetch('http://api-gateway/orders')
fetch('http://api-gateway/payments')
Single Entry Point
Clients only know about one URL. Gateway handles routing.
Cross-Cutting Concerns
Authentication, logging, rate limiting - in one place.
Protocol Translation
HTTP to gRPC, REST to WebSocket - gateway handles it.
Spring Cloud Gateway Setup
<!-- pom.xml -->
<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>
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
Route Configuration
# application.yml
spring:
cloud:
gateway:
routes:
# Route to User Service
- id: user-service
uri: lb://user-service # lb = load balanced via Eureka
predicates:
- Path=/users/**
filters:
- StripPrefix=0
# Route to Order Service
- id: order-service
uri: lb://order-service
predicates:
- Path=/orders/**
# Route with path rewrite
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/products/**
filters:
- RewritePath=/api/products/(?<segment>.*), /products/${segment}
Predicates: When to Route
spring:
cloud:
gateway:
routes:
- id: example-route
uri: lb://my-service
predicates:
# Path matching
- Path=/api/**
# HTTP method
- Method=GET,POST
# Header presence
- Header=X-Request-Id
# Query parameter
- Query=category
# Time-based routing
- After=2024-01-01T00:00:00Z
- Before=2025-01-01T00:00:00Z
- Between=2024-01-01T00:00:00Z, 2024-12-31T23:59:59Z
# Host matching
- Host=**.example.com
# Cookie
- Cookie=session, abc123
# Weight (for canary deployments)
# - Weight=group1, 80 (80% traffic)
Filters: Transform Requests/Responses
spring:
cloud:
gateway:
routes:
- id: filter-example
uri: lb://my-service
predicates:
- Path=/api/**
filters:
# Add headers
- AddRequestHeader=X-Request-Source, gateway
- AddResponseHeader=X-Response-Time, 123ms
# Remove headers
- RemoveRequestHeader=Cookie
- RemoveResponseHeader=X-Internal-Header
# Rewrite path
- RewritePath=/api/(?<segment>.*), /${segment}
- StripPrefix=1
- PrefixPath=/v2
# Retry on failure
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY
# Circuit breaker
- name: CircuitBreaker
args:
name: myCircuitBreaker
fallbackUri: forward:/fallback
Rate Limiting
<!-- Add Redis for rate limiting -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
spring:
cloud:
gateway:
routes:
- id: rate-limited-route
uri: lb://user-service
predicates:
- Path=/users/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10 # 10 requests/second
redis-rate-limiter.burstCapacity: 20 # Allow burst up to 20
key-resolver: "#{@userKeyResolver}" # Rate limit by user
@Configuration
public class RateLimiterConfig {
@Bean
public KeyResolver userKeyResolver() {
// Rate limit by user header or IP
return exchange -> Mono.just(
exchange.getRequest().getHeaders()
.getFirst("X-User-Id") != null
? exchange.getRequest().getHeaders().getFirst("X-User-Id")
: exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
);
}
}
Authentication Filter
@Component
public class AuthenticationFilter implements GlobalFilter, Ordered {
@Autowired
private JwtUtil jwtUtil;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// Skip auth for public paths
if (isPublicPath(request.getPath().toString())) {
return chain.filter(exchange);
}
// Check Authorization header
String authHeader = request.getHeaders().getFirst("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return unauthorized(exchange);
}
String token = authHeader.substring(7);
try {
String userId = jwtUtil.validateAndGetUserId(token);
// Add user info to request for downstream services
ServerHttpRequest modifiedRequest = request.mutate()
.header("X-User-Id", userId)
.build();
return chain.filter(exchange.mutate().request(modifiedRequest).build());
} catch (Exception e) {
return unauthorized(exchange);
}
}
private Mono<Void> unauthorized(ServerWebExchange exchange) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return -1; // Run before other filters
}
}
Fallback Handler
@RestController
public class FallbackController {
@GetMapping("/fallback")
public ResponseEntity<Map<String, String>> fallback() {
Map<String, String> response = new HashMap<>();
response.put("status", "Service temporarily unavailable");
response.put("message", "Please try again later");
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(response);
}
@GetMapping("/fallback/users")
public ResponseEntity<List<User>> userFallback() {
// Return cached data or empty list
return ResponseEntity.ok(Collections.emptyList());
}
}
Programmatic Route Configuration
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r
.path("/users/**")
.filters(f -> f
.addRequestHeader("X-Gateway", "spring-cloud")
.retry(3))
.uri("lb://user-service"))
.route("order-service", r -> r
.path("/orders/**")
.and()
.method(HttpMethod.GET, HttpMethod.POST)
.filters(f -> f
.circuitBreaker(c -> c
.setName("orderCircuitBreaker")
.setFallbackUri("forward:/fallback/orders")))
.uri("lb://order-service"))
.build();
}
}