Lab: Spring Cloud API Gateway
This lab will help you understand the key concepts of API Gateway within the Spring Cloud ecosystem.
Introduction
API Gateways serve as a critical component in modern microservices architectures. They act as a single entry point for all client requests, routing them to appropriate services while providing cross-cutting concerns like authentication, monitoring, and load balancing.
Lab Objectives
By the end of this lab, you’ll understand:
- What an API Gateway is and why it’s essential in microservices
- How Spring Cloud API Gateway works
- How to implement routing, filtering, and other gateway features
- How to integrate API Gateway with a service registry
Prerequisites
- Basic knowledge of Spring Boot
- Java 17 or higher installed
- Maven or Gradle
- An IDE (IntelliJ IDEA, Eclipse, or VS Code)
Part 1: Setting Up the Project Structure
We’ll create three Spring Boot applications:
- An API Gateway
- Two simple microservices that our gateway will route to
Step 1: Create the Service Projects
First, let’s create two simple service applications. You can use Spring Initializr (https://start.spring.io/) with these settings:
For product-service:
- Project: Maven
- Language: Java
- Spring Boot: 3.2.x
- Dependencies: Spring Web, Spring Boot Actuator
For order-service:
- Project: Maven
- Language: Java
- Spring Boot: 3.2.x
- Dependencies: Spring Web, Spring Boot Actuator
Download these projects and extract them to your workspace.
Step 2: Create the API Gateway Project
Create another Spring Boot application for our API Gateway:
- Project: Maven
- Language: Java
- Spring Boot: 3.2.x
- Dependencies: Gateway, Eureka Discovery Client, Spring Boot Actuator
Part 2: Implementing the Microservices
Let’s implement our simple services first.
Step 3: Configure the Product Service
Open the product-service project and modify the application.properties
file:
server.port=8081
spring.application.name=product-service
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
Create a controller in src/main/java/com/example/productservice/controller
:
package com.example.productservice.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/products")
public class ProductController {
private List<Product> products = Arrays.asList(
new Product(1, "Laptop", 1299.99),
new Product(2, "Smartphone", 699.99),
new Product(3, "Headphones", 149.99)
);
@GetMapping
public List<Product> getAllProducts() {
return products;
}
@GetMapping("/{id}")
public Product getProductById(@PathVariable int id) {
return products.stream()
.filter(product -> product.getId() == id)
.findFirst()
.orElseThrow(() -> new RuntimeException("Product not found"));
}
static class Product {
private int id;
private String name;
private double price;
public Product(int id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
}
Step 4: Configure the Order Service
Open the order-service project and modify the application.properties
file:
server.port=8082
spring.application.name=order-service
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
Create a controller in src/main/java/com/example/orderservice/controller
:
package com.example.orderservice.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/orders")
public class OrderController {
private List<Order> orders = Arrays.asList(
new Order(1, "ORD001", 1299.99),
new Order(2, "ORD002", 699.99),
new Order(3, "ORD003", 149.99)
);
@GetMapping
public List<Order> getAllOrders() {
return orders;
}
@GetMapping("/{id}")
public Order getOrderById(@PathVariable int id) {
return orders.stream()
.filter(order -> order.getId() == id)
.findFirst()
.orElseThrow(() -> new RuntimeException("Order not found"));
}
static class Order {
private int id;
private String orderNumber;
private double total;
public Order(int id, String orderNumber, double total) {
this.id = id;
this.orderNumber = orderNumber;
this.total = total;
}
public int getId() {
return id;
}
public String getOrderNumber() {
return orderNumber;
}
public double getTotal() {
return total;
}
}
}
Part 3: Setting Up Service Discovery
To make things more realistic, we’ll add a service registry using Netflix Eureka.
Step 5: Create Eureka Server
Create a new Spring Boot application:
- Project: Maven
- Language: Java
- Spring Boot: 3.2.x
- Dependencies: Eureka Server
Configure application.properties
:
server.port=8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
spring.application.name=eureka-server
Enable Eureka Server in the main application class by adding @EnableEurekaServer
annotation:
package com.example.eurekaserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
Part 4: Implementing the API Gateway
Now let’s implement our API Gateway service.
Step 6: Configure the API Gateway
Open the api-gateway project and update the application.properties
file:
server.port=8080
spring.application.name=api-gateway
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
# Enabling discovery locator to automatically create routes based on service IDs
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
# Configuring manual routes
spring.cloud.gateway.routes[0].id=product-service
spring.cloud.gateway.routes[0].uri=lb://product-service
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/products/**
spring.cloud.gateway.routes[0].filters[0]=StripPrefix=1
spring.cloud.gateway.routes[1].id=order-service
spring.cloud.gateway.routes[1].uri=lb://order-service
spring.cloud.gateway.routes[1].predicates[0]=Path=/api/orders/**
spring.cloud.gateway.routes[1].filters[0]=StripPrefix=1
# Actuator exposure for monitoring
management.endpoints.web.exposure.include=*
Step 7: Create a Simple Filter
Create a custom filter to add a request header to all outgoing requests:
package com.example.apigateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
@Component
public class LoggingFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("Path requested: " + exchange.getRequest().getPath());
// Add a request header
return chain.filter(
exchange.mutate()
.request(
exchange.getRequest()
.mutate()
.header("X-Request-Time", LocalDateTime.now().toString())
.build()
)
.build()
);
}
}
Step 8: Create a Custom Route Config (Alternative to property-based config)
As an alternative to the property-based configuration, we can define routes programmatically:
package com.example.apigateway.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("product_route", r -> r
.path("/api/products/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://product-service"))
.route("order_route", r -> r
.path("/api/orders/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://order-service"))
.build();
}
}
To use this configuration instead of the property-based one, comment out the route configuration in the properties file and uncomment the code above.
Part 5: Running and Testing the Application
Step 9: Build and Run All Services
Start the services in this order:
- Eureka Server
- Product Service
- Order Service
- API Gateway
You can run each service in a separate terminal window using:
mvn spring-boot:run
Step 10: Test the Gateway
Now, test your API Gateway using a tool like Postman or curl:
# Test direct product service
curl http://localhost:8081/products
# Test direct order service
curl http://localhost:8082/orders
# Test via API Gateway - product service
curl http://localhost:8080/api/products
# Test via API Gateway - order service
curl http://localhost:8080/api/orders
# Test getting a specific product
curl http://localhost:8080/api/products/1
# Test getting a specific order
curl http://localhost:8080/api/orders/2
Part 6: Advanced Features (Optional Extensions)
Step 11: Add Rate Limiting
Add a rate limiter to the API Gateway to limit the number of requests:
Add the Redis dependency to your pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
Configure rate limiting in your properties file:
# Rate limiting configuration
spring.cloud.gateway.routes[0].filters[1]=name=RequestRateLimiter,args[redis-rate-limiter.replenishRate]=10,args[redis-rate-limiter.burstCapacity]=20
Or in your route configuration:
.route("product_route", r -> r
.path("/api/products/**")
.filters(f -> f
.stripPrefix(1)
.requestRateLimiter(config -> config
.setRateLimiter(redisRateLimiter())
.setKeyResolver(userKeyResolver())))
.uri("lb://product-service"))
Step 12: Add Circuit Breaker
Implement a circuit breaker pattern to handle service failures gracefully:
Add the Resilience4j dependency:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
Configure circuit breaker:
.route("product_route", r -> r
.path("/api/products/**")
.filters(f -> f
.stripPrefix(1)
.circuitBreaker(config -> config
.setName("productServiceCircuitBreaker")
.setFallbackUri("forward:/fallback/products")))
.uri("lb://product-service"))
Add a fallback controller:
package com.example.apigateway.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/fallback")
public class FallbackController {
@GetMapping("/products")
public Map<String, String> productServiceFallback() {
Map<String, String> response = new HashMap<>();
response.put("message", "Product Service is currently unavailable. Please try again later.");
return response;
}
@GetMapping("/orders")
public Map<String, String> orderServiceFallback() {
Map<String, String> response = new HashMap<>();
response.put("message", "Order Service is currently unavailable. Please try again later.");
return response;
}
}
Lab Exercises for Students
-
Basic Exercise: Add a new microservice (e.g., user-service) and configure the API Gateway to route to it.
-
Intermediate Exercise: Implement a custom filter that adds authentication headers to certain routes.
-
Advanced Exercise: Implement a rate limiter and circuit breaker for all routes and test their behavior under high load or when services are down.
Discussion Questions
-
How does API Gateway simplify client interactions in a microservices architecture?
-
What are the advantages and potential drawbacks of using an API Gateway?
-
How does Spring Cloud API Gateway compare to other API Gateway solutions like Netflix Zuul or Kong?
-
In what scenarios might you choose to use property-based route configuration versus programmatic configuration?
-
How would you secure your API Gateway in a production environment?
Key Concepts to Understand
-
Routing: The primary function of directing requests to appropriate services.
-
Filtering: Adding cross-cutting concerns like logging, security, and transformation.
-
Service Discovery Integration: Automatically finding and routing to available service instances.
-
Load Balancing: Distributing traffic across multiple instances of a service.
-
Circuit Breaking: Preventing cascading failures when services are down.
-
Rate Limiting: Protecting services from overload by limiting request rates.
By Wahid Hamdi