Making REST Calls

In a Spring Boot application, there are several standard ways to make REST calls. The choice of approach depends on your requirements, such as simplicity, performance, or flexibility. Here are the main methods:

1. RestTemplate

  • A widely used class in Spring for making synchronous REST calls.
  • Recommended for simple and straightforward HTTP requests.
  • Status: Deprecated for new development in favor of WebClient.

Example:

import org.springframework.web.client.RestTemplate;

RestTemplate restTemplate = new RestTemplate();
String url = "https://api.example.com/data";

// GET Request
String response = restTemplate.getForObject(url, String.class);

// POST Request
MyResponse response = restTemplate.postForObject(url, new MyRequest(), MyResponse.class);

Dependencies: Included in Spring Boot Starter Web (spring-boot-starter-web).


2. WebClient

  • Part of the Spring WebFlux library, supports both synchronous and asynchronous programming.
  • Ideal for reactive applications and modern projects.
  • Status: Recommended for new development.

Example:

import org.springframework.web.reactive.function.client.WebClient;

WebClient webClient = WebClient.builder().baseUrl("https://api.example.com").build();

// GET Request
String response = webClient.get()
    .uri("/data")
    .retrieve()
    .bodyToMono(String.class)
    .block(); // For synchronous processing

// POST Request
MyResponse response = webClient.post()
    .uri("/data")
    .bodyValue(new MyRequest())
    .retrieve()
    .bodyToMono(MyResponse.class)
    .block();

Dependencies: Requires spring-boot-starter-webflux.


3. Apache HttpClient

  • Provides fine-grained control over HTTP requests.
  • Useful for advanced configurations and tuning.

Example:

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet request = new HttpGet("https://api.example.com/data");

try (CloseableHttpResponse response = httpClient.execute(request)) {
    System.out.println(EntityUtils.toString(response.getEntity()));
}

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
</dependency>

4. OkHttpClient

  • Lightweight and efficient HTTP client.
  • Frequently used in microservices or high-performance systems.

Example:

import okhttp3.*;

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url("https://api.example.com/data").build();

try (Response response = client.newCall(request).execute()) {
    System.out.println(response.body().string());
}

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.10.0</version>
</dependency>

5. Feign Client

  • Declarative REST client, part of Spring Cloud.
  • Ideal for microservice communication.

Example:

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "apiClient", url = "https://api.example.com")
public interface ApiClient {
    @GetMapping("/data")
    String getData();
}

// Usage in a Service
@Autowired
private ApiClient apiClient;

String response = apiClient.getData();

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

6. Java 11 HttpClient

  • Part of Java standard library, introduced in Java 11.
  • Supports asynchronous programming using CompletableFuture.

Example:

import java.net.http.*;

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/data"))
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());

Feign Client is recommended over other methods like RestTemplate, WebClient, or manual HTTP clients when developing microservices because of the following reasons:


1. Declarative and Simplified Approach

  • Feign provides a declarative style for defining REST API calls using interfaces and annotations.
  • It eliminates boilerplate code for making HTTP requests, improving readability and maintainability.

Example: With Feign:

@FeignClient(name = "userClient", url = "https://api.example.com/users")
public interface UserClient {
    @GetMapping("/{id}")
    User getUserById(@PathVariable("id") Long id);
}

// Usage
User user = userClient.getUserById(1L);

Without Feign (e.g., RestTemplate):

RestTemplate restTemplate = new RestTemplate();
String url = "https://api.example.com/users/1";

User user = restTemplate.getForObject(url, User.class);

Explanation: Feign reduces verbosity by handling URL construction, serialization/deserialization, and error handling internally.


2. Built-in Load Balancing (Ribbon Integration)

  • Feign integrates seamlessly with Spring Cloud Load Balancer or other client-side load balancers like Ribbon.
  • Feign Clients can discover and communicate with other services using service names rather than hardcoding URLs.

Example with Load Balancer:

@FeignClient(name = "user-service") // No need to specify base URL
public interface UserServiceClient {
    @GetMapping("/users/{id}")
    User getUserById(@PathVariable("id") Long id);
}

How It Works:

  • The service name user-service is resolved to the actual URL by the load balancer.
  • This is especially useful in distributed systems where service instances can scale dynamically.

3. Automatic Serialization and Deserialization

  • Feign uses Jackson (or other configured libraries) to serialize request bodies and deserialize responses automatically.
  • Saves time compared to manual parsing.

Example:

@FeignClient(name = "order-service")
public interface OrderClient {
    @PostMapping("/orders")
    OrderResponse createOrder(@RequestBody OrderRequest request);
}

// JSON is handled automatically
OrderRequest request = new OrderRequest("item123", 2);
OrderResponse response = orderClient.createOrder(request);

Without Feign:

  • Developers need to manually create HTTP headers, handle JSON serialization, and parse the response.

4. Integration with Spring Boot Features

  • Feign supports:
    • Circuit Breaker (e.g., using Resilience4j or Hystrix).
    • Retry mechanisms.
    • Custom error handling.
  • These integrations enhance resilience and fault tolerance in microservices.

Example with Circuit Breaker:

@FeignClient(name = "inventory-service", fallback = InventoryFallback.class)
public interface InventoryClient {
    @GetMapping("/inventory/{itemId}")
    Inventory getInventory(@PathVariable("itemId") String itemId);
}

// Fallback implementation
@Component
public class InventoryFallback implements InventoryClient {
    @Override
    public Inventory getInventory(String itemId) {
        return new Inventory(itemId, 0); // Return default inventory
    }
}

How It Helps:

  • If inventory-service is down, the fallback logic is automatically executed, ensuring graceful degradation.

5. Easy Integration with Service Registries

  • Feign works seamlessly with service registries like Eureka or Consul, allowing clients to discover services dynamically.

Example with Eureka:

@FeignClient(name = "user-service") // Eureka resolves this name to a running instance
public interface UserClient {
    @GetMapping("/users/{id}")
    User getUser(@PathVariable Long id);
}
  • No need to hardcode service URLs; Feign resolves the service name using the service registry.

6. Extensible and Customizable

  • Feign allows adding custom configurations like interceptors, logging, and request templates.

Example: Adding Custom Interceptor:

@Configuration
public class FeignConfig {
    @Bean
    public RequestInterceptor requestInterceptor() {
        return requestTemplate -> {
            requestTemplate.header("Authorization", "Bearer <token>");
        };
    }
}

@FeignClient(name = "secure-service", configuration = FeignConfig.class)
public interface SecureServiceClient {
    @GetMapping("/secure-data")
    String getSecureData();
}

Explanation:

  • The interceptor automatically adds a Bearer token to every request, ensuring consistent authentication across calls.

Why Not Other Methods?

FeatureFeignRestTemplateWebClientApache/OkHttp
Declarative Calls✅ Simplified❌ Requires manual calls❌ Requires manual calls❌ Requires manual calls
Service Discovery✅ Built-in❌ Additional setup❌ Additional setup❌ Additional setup
Serialization/Deserialization✅ Automatic✅ Partial (manual configs)✅ Automatic❌ Manual
Circuit Breaker Support✅ Easy Integration❌ No direct support❌ No direct support❌ No direct support
Ease of Use✅ High❌ Moderate❌ Moderate❌ Low
Reactive Programming❌ No❌ No✅ Yes❌ No

Conclusion

  • Feign is best suited for microservice-to-microservice communication in a Spring Cloud ecosystem.
  • It reduces boilerplate code, supports service discovery and fault tolerance, and integrates seamlessly with Spring Cloud’s features.
  • For modern distributed systems, it simplifies and accelerates development while enhancing scalability and resilience.