How to Implement an API Gateway in Spring Boot
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
- Mixing WebFlux and MVC dependencies. Spring Cloud Gateway requires a reactive stack; adding
spring-boot-starter-webbreaks route matching in subtle ways. - Putting business logic in gateway filters. The gateway should route, authenticate, and rate-limit — not transform payloads or make domain decisions.
- No timeout configuration. Set
spring.cloud.gateway.httpclient.connect-timeoutandresponse-timeoutexplicitly; the defaults are often too generous for production. - Skipping observability. Add Micrometer and export request counts, latencies, and route match failures — debugging a gateway without metrics is painful.
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.