API Gateway
An API gateway is the single front door to your microservices. Instead of exposing a dozen services to clients, you expose one address that routes, authenticates, rate-limits, and shapes traffic on the way in. Spring Cloud Gateway is the Spring way to build it — a reactive, non-blocking gateway built on Spring WebFlux and Project Reactor.
Why a gateway
Without a gateway, every client must know every service’s address, and each service must independently solve auth, CORS, and rate limiting. A gateway centralizes those cross-cutting concerns:
┌───────────────────────────┐
clients ─────► │ API Gateway │
│ auth · rate-limit · CORS │
└───┬──────────┬──────────┬──┘
│ │ │
lb://orders lb://inventory lb://payments
Dependency
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- to resolve lb:// against the registry -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
Warning: Spring Cloud Gateway is reactive — do not add
spring-boot-starter-web(servlet) to the same app, or startup fails on a conflicting web stack. Usespring-boot-starter-webfluxsemantics throughout the gateway service.
Routes in YAML
A route has an id, a uri (destination), predicates (when it matches), and filters (how to transform it).
spring:
cloud:
gateway:
routes:
- id: orders
uri: lb://orders-service # resolved via discovery + load balancer
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1 # drop /api before forwarding
- id: inventory
uri: lb://inventory-service
predicates:
- Path=/api/inventory/**
filters:
- StripPrefix=1
A request to GET /api/orders/42 matches the first route and is forwarded to a discovered instance of orders-service as GET /orders/42.
Routes in Java (RouteLocator)
For dynamic or programmatic routing, define a RouteLocator bean:
@Configuration
public class GatewayRoutes {
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("orders", r -> r
.path("/api/orders/**")
.filters(f -> f.stripPrefix(1)
.addRequestHeader("X-Gateway", "edge"))
.uri("lb://orders-service"))
.route("inventory", r -> r
.path("/api/inventory/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://inventory-service"))
.build();
}
}
Predicates and filters
Predicates decide whether a route matches; filters modify the request or response.
| Predicate | Matches on |
|---|---|
Path=/api/orders/** | Request path |
Method=GET,POST | HTTP method |
Header=X-Tenant, \d+ | Header value (regex) |
Query=debug | Query parameter present |
After=2026-01-01T00:00:00Z | Time window |
| Filter | Effect |
|---|---|
StripPrefix=1 | Remove leading path segments |
AddRequestHeader=K,V | Add a request header |
RewritePath=/api/(?<s>.*), /${s} | Rewrite the path |
CircuitBreaker=name | Wrap the route in a circuit breaker |
RequestRateLimiter | Throttle requests |
Integrating with discovery
The lb://service-name URI scheme tells the gateway to resolve the name against the registry and load-balance across healthy instances — no host or port anywhere. You can even auto-create routes for every registered service:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # route /orders-service/** to it automatically
lower-case-service-id: true
Tip: Auto-discovery routing is handy in development but explicit routes are clearer and safer in production — you control exactly what is exposed.
Cross-cutting concerns
Rate limiting
The built-in Redis rate limiter uses a token-bucket per key (here, per client IP):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10 # tokens per second
redis-rate-limiter.burstCapacity: 20 # max burst
key-resolver: "#{@ipKeyResolver}"
@Bean
KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
Authentication
A global filter is a natural place to validate a JWT once, at the edge, before traffic reaches any service:
@Component
public class AuthFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String auth = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (auth == null || !auth.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// validate token, then continue
return chain.filter(exchange);
}
}
For full OAuth2 validation, make the gateway an OAuth2 resource server.
CORS
Configure CORS centrally so individual services don’t have to:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "https://app.example.com"
allowedMethods: [GET, POST, PUT, DELETE]
allowedHeaders: "*"