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:

  1. An API Gateway
  2. 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:

  1. Eureka Server
  2. Product Service
  3. Order Service
  4. 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

  1. Basic Exercise: Add a new microservice (e.g., user-service) and configure the API Gateway to route to it.

  2. Intermediate Exercise: Implement a custom filter that adds authentication headers to certain routes.

  3. 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

  1. How does API Gateway simplify client interactions in a microservices architecture?

  2. What are the advantages and potential drawbacks of using an API Gateway?

  3. How does Spring Cloud API Gateway compare to other API Gateway solutions like Netflix Zuul or Kong?

  4. In what scenarios might you choose to use property-based route configuration versus programmatic configuration?

  5. How would you secure your API Gateway in a production environment?

Key Concepts to Understand

  1. Routing: The primary function of directing requests to appropriate services.

  2. Filtering: Adding cross-cutting concerns like logging, security, and transformation.

  3. Service Discovery Integration: Automatically finding and routing to available service instances.

  4. Load Balancing: Distributing traffic across multiple instances of a service.

  5. Circuit Breaking: Preventing cascading failures when services are down.

  6. Rate Limiting: Protecting services from overload by limiting request rates.


By Wahid Hamdi