API Gateway

One Door to All Your Microservices

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();
    }
}

Master Microservices Patterns

Learn API gateways, service mesh, and cloud-native architecture.

Explore Full Stack Java Course