← Blog

How to Implement an API Gateway in Spring Boot

2026-09-07 · 4 min read · SubToAPI Team

Implementing an API gateway in Spring Boot almost always means using Spring Cloud Gateway, the routing layer built on Project Reactor that sits in front of your backend services. It handles path-based routing, request/response filtering, load balancing, and cross-cutting concerns like authentication and rate limiting, so individual services don't have to duplicate that logic.

This guide walks through the actual implementation: setting up the project, defining routes, adding filters, and wiring in the pieces most teams need in production (auth, rate limiting, circuit breaking). If you're deciding whether you need a gateway at all, that's a separate question — this is for teams who already know they want one and need working code.

Step 1: Create the Gateway Project

Start a new Spring Boot project with the reactive gateway starter. Using Spring Initializr or your build file directly:

<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
  </dependency>
</dependencies>

Important: don't add spring-boot-starter-web alongside this. Spring Cloud Gateway runs on WebFlux/Netty, not Tomcat, and mixing the two causes startup failures. Match your Spring Cloud version to your Boot version using the BOM:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>2023.0.1</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Step 2: Define Routes

Routes map incoming requests to backend services. You can configure them in YAML or in Java code. YAML is easier to read and change without redeploying logic:

spring:
  cloud:
    gateway:
      routes:
        - id: orders-service
          uri: http://localhost:8081
          predicates:
            - Path=/api/orders/**
          filters:
            - StripPrefix=1
        - id: users-service
          uri: http://localhost:8082
          predicates:
            - Path=/api/users/**

Each route has an id, a target uri, one or more predicates (conditions for matching a request), and optional filters (transformations applied to the request or response). StripPrefix=1 removes /api before forwarding, so /api/orders/42 becomes /orders/42 on the backend.

For dynamic service discovery instead of hardcoded URIs, point the uri at lb://SERVICE-NAME and integrate with Eureka or Consul.

Step 3: Configure Routes in Java (Optional)

If you need conditional logic that YAML can't express, define routes programmatically:

@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator customRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("orders", r -> r.path("/api/orders/**")
                .filters(f -> f.stripPrefix(1)
                    .addRequestHeader("X-Gateway-Source", "internal"))
                .uri("http://localhost:8081"))
            .build();
    }
}

This is useful when routing decisions depend on headers, request bodies, or values pulled from a config service at startup.

Step 4: Add Authentication

Most gateways need to validate a token before forwarding a request. A GlobalFilter runs on every request regardless of route:

@Component
public class AuthFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String authHeader = exchange.getRequest()
            .getHeaders()
            .getFirst(HttpHeaders.AUTHORIZATION);

        if (authHeader == null || !isValid(authHeader)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -1;
    }
}

For JWT validation specifically, Spring Security's resource server support integrates directly with Spring Cloud Gateway and handles signature verification and expiry checks for you — writing that logic by hand is usually not worth it.

Step 5: Add Rate Limiting

Spring Cloud Gateway ships with a Redis-backed rate limiter out of the box:

filters:
  - name: RequestRateLimiter
    args:
      redis-rate-limiter.replenishRate: 10
      redis-rate-limiter.burstCapacity: 20

This requires spring-boot-starter-data-redis-reactive on the classpath and a running Redis instance. replenishRate is the sustained requests-per-second allowed; burstCapacity is the max burst above that rate.

If you're building this same functionality for an LLM-backed service — API keys, per-key rate limits, usage tracking — it's worth checking whether it's faster to build it yourself in Spring Cloud Gateway or to use a service that already provides it. SubToAPI does exactly this for Claude access: it issues sub_live_... API keys, meters usage, and exposes a standard /v1/messages endpoint, so you get the gateway behaviors (auth, rate limits, usage metadata) without writing the filter chain yourself. See the quickstart for what that looks like from the client side.

Step 6: Add Resilience with Circuit Breakers

Wrap routes to a flaky backend with Resilience4j so one failing service doesn't cascade:

filters:
  - name: CircuitBreaker
    args:
      name: ordersCB
      fallbackUri: forward:/fallback/orders

Define a fallback controller that returns a cached response or a graceful error instead of propagating a timeout to the caller.

Step 7: Test the Gateway

Run the gateway and each backend service, then confirm routing works end to end:

curl http://localhost:8080/api/orders/42
curl -H "Authorization: Bearer invalid" http://localhost:8080/api/orders/42

The first should reach the orders service through the gateway; the second should return 401 from your auth filter before ever touching the backend. Also test what happens when a backend is down — the circuit breaker fallback should trigger rather than the request hanging.

Common Mistakes to Avoid

Questions

Do I need Spring Cloud Gateway, or can I use Zuul? Zuul 1 is in maintenance mode and blocking-based; Spring Cloud Gateway is the actively maintained, reactive successor and the default choice for new projects.

Can Spring Cloud Gateway run alongside a traditional Spring MVC app? Not in the same application context — it needs WebFlux/Netty. Run it as a separate service in front of your MVC-based microservices.

Is Spring Cloud Gateway suitable for a single monolith, or only microservices? It's built for routing between multiple services. For a single monolith with one deployable, a gateway adds latency and complexity without a clear benefit.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →