Course topics

By WebNest Studio

Spring Boot Tutorial

API Gateway with Spring Cloud Gateway

Clients should not need to know that your system consists of fifteen services on different hosts. An API gateway gives them one entry point: it routes each request to the right service and handles cross-cutting concerns at the edge — authentication, rate limiting, CORS, request/response rewriting, retries and circuit breaking.

Spring Cloud Gateway is Spring's API gateway, available in a reactive (WebFlux, Netty) variant and a servlet (Spring MVC) variant. This lesson builds a WebFlux-based gateway with routes, predicates and filters, JWT validation at the edge, Redis-backed rate limiting, circuit breakers with fallbacks, and discovery-based routing.

Routes, Predicates and Filters

A route has an id, a destination uri, predicates that decide whether a request matches (Path, Method, Host, Header, Query, Weight for canary releases), and filters that modify the request or response (StripPrefix, RewritePath, AddRequestHeader, RequestRateLimiter, CircuitBreaker, Retry). Routes are defined in YAML under spring.cloud.gateway.server.webflux.routes or in Java with RouteLocatorBuilder.

Choosing a Variant

spring-cloud-starter-gateway-server-webflux runs on Netty and handles very high concurrency with few threads — the classic choice. spring-cloud-starter-gateway-server-webmvc runs on the servlet stack, which suits teams that prefer blocking code (especially with virtual threads). The concepts are the same; the configuration prefixes differ (...server.webflux... vs ...server.webmvc...).

Security at the Edge

Configure the gateway as an OAuth2 resource server to reject requests without a valid JWT before they reach any service, and relay the token downstream (TokenRelay filter when the gateway is also the OAuth2 client for a browser app). Downstream services should still validate tokens themselves — "defence in depth" — rather than trusting anything that comes from inside the network.

Rate Limiting and Resilience

The RequestRateLimiter filter with the Redis rate limiter implements a token bucket per key (user, API key or IP) that works across multiple gateway instances. The CircuitBreaker filter (Resilience4j) returns a fallback when a service is failing, and the Retry filter retries idempotent requests on transient errors.

Examples

Gateway routes in YAML

Java
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>

# application.yml
server:
  port: 8080
spring:
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: catalog
              uri: http://catalog-service:8081
              predicates:
                - Path=/api/catalog/**
              filters:
                - RewritePath=/api/catalog/(?<rest>.*), /api/products/${rest}
            - id: orders
              uri: http://order-service:8082
              predicates:
                - Path=/api/orders/**
                - Method=GET,POST
              filters:
                - AddRequestHeader=X-Gateway, webnest
            - id: new-checkout-canary
              uri: http://checkout-v2:8090
              predicates:
                - Path=/api/checkout/**
                - Weight=checkout, 10          # 10% of traffic to v2
            - id: checkout-stable
              uri: http://checkout-v1:8089
              predicates:
                - Path=/api/checkout/**
                - Weight=checkout, 90
Output
GET http://gateway:8080/api/catalog/42    -> forwarded to http://catalog-service:8081/api/products/42
POST http://gateway:8080/api/orders       -> forwarded to order-service with header X-Gateway: webnest
/api/checkout/**                          -> ~90% to checkout-v1, ~10% to checkout-v2

JWT validation, rate limiting and circuit breaking

Java
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>

@Configuration
@EnableWebFluxSecurity
public class GatewaySecurity {

    @Bean
    SecurityWebFilterChain security(ServerHttpSecurity http) {
        return http
            .authorizeExchange(ex -> ex
                .pathMatchers("/api/catalog/**").permitAll()
                .anyExchange().authenticated())
            .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
            .csrf(ServerHttpSecurity.CsrfSpec::disable)
            .build();
    }

    // rate-limit per authenticated user
    @Bean
    KeyResolver userKeyResolver() {
        return exchange -> exchange.getPrincipal().map(Principal::getName).defaultIfEmpty("anonymous");
    }
}

# application.yml (route filters)
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://auth-server:9000
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: orders
              uri: http://order-service:8082
              predicates:
                - Path=/api/orders/**
              filters:
                - name: RequestRateLimiter
                  args:
                    redis-rate-limiter.replenishRate: 10     # tokens per second
                    redis-rate-limiter.burstCapacity: 20
                    key-resolver: "#{@userKeyResolver}"
                - name: CircuitBreaker
                  args:
                    name: orders
                    fallbackUri: forward:/fallback/orders

@RestController
class FallbackController {
    @GetMapping("/fallback/orders")
    Mono<ResponseEntity<Map<String, String>>> ordersDown() {
        return Mono.just(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(Map.of("message", "Orders are temporarily unavailable. Please try again shortly.")));
    }
}
Output
GET /api/orders (no token)                -> 401 Unauthorized (rejected at the gateway)
GET /api/orders (valid JWT, 25 req/s)     -> first 20 OK, then 429 Too Many Requests
GET /api/orders (order-service down)      -> 503 {"message":"Orders are temporarily unavailable. Please try again shortly."}

Routes in Java and discovery-based URIs

Java
@Bean
RouteLocator routes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route("payments", r -> r.path("/api/payments/**")
            .filters(f -> f.retry(c -> c.setRetries(2).setMethods(HttpMethod.GET)))
            .uri("lb://payment-service"))           // resolved through Eureka / LoadBalancer
        .build();
}
Output
GET /api/payments/pay_91x -> lb://payment-service -> 10.0.0.12:53120 (retried on another instance if it fails)

Common Mistakes

  • Using the old spring.cloud.gateway.routes prefix; current versions use spring.cloud.gateway.server.webflux.routes (or ...webmvc...).
  • Putting business logic in the gateway, turning it into a new monolith.
  • Validating JWTs only at the gateway and trusting all internal traffic blindly.
  • Retrying non-idempotent POST requests at the gateway.
  • Deploying the WebFlux gateway as a WAR or into a servlet container — it requires Netty.

Key Points to Remember

  • Spring Cloud Gateway routes requests using predicates and modifies them with filters.
  • Choose the WebFlux (Netty) or WebMVC variant; routes live under spring.cloud.gateway.server.<variant>.routes.
  • Validate JWTs at the edge and again in services.
  • RequestRateLimiter with Redis and CircuitBreaker with fallbacks protect services.
  • lb:// URIs route through service discovery; Weight predicates enable canary releases.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.