Skip to content
Spring Boot sb microservices 3 min read

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. Use spring-boot-starter-webflux semantics 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.

PredicateMatches on
Path=/api/orders/**Request path
Method=GET,POSTHTTP method
Header=X-Tenant, \d+Header value (regex)
Query=debugQuery parameter present
After=2026-01-01T00:00:00ZTime window
FilterEffect
StripPrefix=1Remove leading path segments
AddRequestHeader=K,VAdd a request header
RewritePath=/api/(?<s>.*), /${s}Rewrite the path
CircuitBreaker=nameWrap the route in a circuit breaker
RequestRateLimiterThrottle 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: "*"
Last updated June 13, 2026
Was this helpful?