“Your Spring Boot app can talk to the world, not just your browser! Think of it as teaching your monkey to order bananas from an online store, not just pick them from a tree.” 🐒🛒

You’ve built robust APIs, but modern applications rarely live in isolation. They need to fetch data, trigger actions, or interact with other services—often via their REST APIs. Enter Spring Boot’s RestClient, the modern, non-blocking way to consume external HTTP services. Let’s learn to make your application a fluent speaker in the language of the web!


🚩 Prerequisites

  1. Java Development Kit (JDK) 17+
  2. Maven or Gradle
  3. A basic Spring Boot REST project (like the one from “How to set up your first Spring Boot project”).
  4. Familiarity with REST API concepts (GET, POST, JSON).

1️⃣ Why RestClient? (Goodbye RestTemplate!)

For years, RestTemplate was the go-to for synchronous HTTP calls. But it’s now in maintenance mode, superseded by more modern alternatives:

  • WebClient: Reactive, non-blocking, and powerful. Great for complex async scenarios.
  • RestClient: New in Spring Framework 6 (Spring Boot 3.2+). It’s a synchronous, blocking client with a fluent API, designed to be a modern replacement for RestTemplate, offering a similar feel but built on WebClient‘s infrastructure.

For most simple, blocking API calls, RestClient is your new best friend.


2️⃣ Add Dependencies (If Not Already There)

If you’re using Spring Boot 3.2+ and have spring-boot-starter-web, RestClient is already available. If not, ensure you have the Web Starter:

Maven (pom.xml)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Gradle (build.gradle)

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

3️⃣ Configure RestClient as a Bean

It’s best practice to configure and provide RestClient as a bean, allowing for easy injection and customization (e.g., base URL, timeouts).

// src/main/java/com/example/demo/config/AppConfig.java
package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

@Configuration
public class AppConfig {

    @Bean
    public RestClient externalApiService(RestClient.Builder builder) {
        return builder
                .baseUrl("https://api.example.com") // Base URL for the external API
                .build();
    }

    // You can also create a generic RestClient builder for other services if needed
    @Bean
    public RestClient.Builder restClientBuilder() {
        return RestClient.builder();
    }
}
  • RestClient.Builder: This is automatically provided by Spring Boot.
  • baseUrl(): Crucial for setting the common part of your external API’s URL.

4️⃣ Create a Service to Consume the API (The Banana Supplier!)

Let’s imagine an external API that provides details about bananas. We’ll define a simple DTO to match its response structure.

// src/main/java/com/example/demo/dto/ExternalBananaDto.java
package com.example.demo.dto;

public record ExternalBananaDto(
    String id,
    String type,
    String color,
    int sweetnessLevel
) {}

// src/main/java/com/example/demo/service/ExternalBananaService.java
package com.example.demo.service;

import com.example.demo.dto.ExternalBananaDto;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import org.springframework.http.MediaType;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

@Service
public class ExternalBananaService {

    private final RestClient restClient;

    public ExternalBananaService(RestClient externalApiService) {
        this.restClient = externalApiService;
    }

    public List<ExternalBananaDto> getAllExternalBananas() {
        return Arrays.asList(restClient.get()
                .uri("/bananas") // Relative to the baseUrl
                .accept(MediaType.APPLICATION_JSON)
                .retrieve() // Execute the request
                .body(ExternalBananaDto[].class)); // Convert response body to an array
    }

    public Optional<ExternalBananaDto> getExternalBananaById(String bananaId) {
        return restClient.get()
                .uri("/bananas/{id}", bananaId)
                .accept(MediaType.APPLICATION_JSON)
                .retrieve()
                .toEntity(ExternalBananaDto.class) // Retrieve as ResponseEntity
                .map(responseEntity -> {
                    if (responseEntity.getStatusCode().is2xxSuccessful()) {
                        return Optional.ofNullable(responseEntity.getBody());
                    }
                    return Optional.<ExternalBananaDto>empty(); // Handle non-2xx status explicitly
                })
                .orElse(Optional.empty()); // Handle no response or other issues
    }
    
    public ExternalBananaDto createExternalBanana(ExternalBananaDto newBanana) {
        return restClient.post()
                .uri("/bananas")
                .contentType(MediaType.APPLICATION_JSON)
                .body(newBanana)
                .retrieve()
                .body(ExternalBananaDto.class);
    }
}
  • get(), post(), put(), delete(): Start building your HTTP request.
  • uri(...): Specifies the path, can use placeholders like {id}.
  • accept(...), contentType(...): Set request headers like Accept and Content-Type.
  • retrieve(): Initiates the exchange and allows you to specify how to handle the response.
  • body(Class<T>): Converts the response body directly to your desired object.
  • toEntity(Class<T>): Returns a ResponseEntity<T>, giving you access to headers and status codes.

5️⃣ Expose via a Controller (Your Banana Portal!)

Finally, expose an endpoint in your application that uses the ExternalBananaService:

// src/main/java/com/example/demo/controller/LocalBananaController.java
package com.example.demo.controller;

import com.example.demo.dto.ExternalBananaDto;
import com.example.demo.service.ExternalBananaService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/local-api/external-bananas")
public class LocalBananaController {

    private final ExternalBananaService externalBananaService;

    public LocalBananaController(ExternalBananaService externalBananaService) {
        this.externalBananaService = externalBananaService;
    }

    @GetMapping
    public List<ExternalBananaDto> getExternalBananas() {
        return externalBananaService.getAllExternalBananas();
    }

    @GetMapping("/{id}")
    public ResponseEntity<ExternalBananaDto> getExternalBanana(@PathVariable String id) {
        return externalBananaService.getExternalBananaById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }
    
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public ExternalBananaDto createExternalBanana(@RequestBody ExternalBananaDto newBanana) {
        return externalBananaService.createExternalBanana(newBanana);
    }
}

6️⃣ Error Handling with RestClient

RestClient provides excellent error handling capabilities. By default, it throws a RestClientException for 4xx and 5xx responses. You can customize this:

Using onStatus()

// In ExternalBananaService
public Optional<ExternalBananaDto> getExternalBananaByIdWithCustomError(String bananaId) {
    return restClient.get()
            .uri("/bananas/{id}", bananaId)
            .accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .onStatus(HttpStatus.NOT_FOUND, (request, response) -> {
                throw new CustomNotFoundException("Banana not found on external API: " + bananaId);
            })
            .onStatus(HttpStatus.INTERNAL_SERVER_ERROR, (request, response) -> {
                throw new RuntimeException("External API server error!");
            })
            .body(ExternalBananaDto.class) // Direct conversion will work if status is OK
            .map(Optional::of) // Wrap in Optional
            .orElse(Optional.empty()); // If body is null or 4xx/5xx handled, return empty
}
  • onStatus(HttpStatus, (req, res) -> ...): Lets you define specific behavior for certain HTTP status codes.

💡 Monkey-Proof Tips

  • Timeouts: Always configure timeouts for external API calls to prevent your application from hanging. Use builder.requestFactory(...) to set read and connection timeouts.
  • Retries/Circuit Breakers: For production, integrate with resilience patterns like retries (e.g., Spring Retry) or circuit breakers (e.g., Resilience4j) to handle transient failures in external services.
  • Authentication: Include authentication headers (API keys, JWTs) using .header("Authorization", "Bearer YOUR_TOKEN") if the external API requires it.
  • Testing: When testing services that use RestClient, mock the RestClient itself or use libraries like WireMock to simulate external API responses.
  • Logging: Add logging interceptors to your RestClient builder to log requests and responses for debugging.

🚀 Challenge

  1. Integrate with a real public API: Replace “https://api.example.com” with a real, free public API (e.g., JSONPlaceholder for dummy data, or a weather API). Adjust your DTOs and service methods accordingly.
  2. Implement Request Timeouts: Configure your RestClient bean to have a connection timeout of 1 second and a read timeout of 5 seconds.
  3. Custom Error Handling for 401 Unauthorized: Add an onStatus() handler for HttpStatus.UNAUTHORIZED (401) that throws a custom ExternalServiceUnauthorizedException. Test this by making a request that would trigger a 401 (if the API supports it, or by simulating it with a test setup).

👏 You’ve now taught your Spring Boot application to seamlessly communicate with external REST APIs using RestClient! Your app is no longer an island; it’s a social butterfly, exchanging data with services across the web. Keep connecting, and keep building interconnected Java applications!

By admin

Leave a Reply

Your email address will not be published. Required fields are marked *