Calling Other Services

RestClient

The synchronous client since Spring 6.1 — fluent, blocking, the default choice in MVC apps.

@Configuration
class ClientConfig {
    @Bean
    RestClient booksClient(RestClient.Builder builder) {
        return builder.baseUrl("https://api.example.com").build();
    }
}
Book book = client.get()
        .uri("/books/{id}", id)
        .retrieve()
        .body(Book.class);

List<Book> all = client.get()
        .uri("/books?author={a}", author)
        .retrieve()
        .body(new ParameterizedTypeReference<>() {});

Book created = client.post()
        .uri("/books")
        .contentType(MediaType.APPLICATION_JSON)
        .body(new BookRequest("Dune", "Herbert", 1965))
        .retrieve()
        .body(Book.class);

Handling failures:

var book = client.get().uri("/books/{id}", id)
        .retrieve()
        .onStatus(HttpStatusCode::is4xxClientError,
                  (req, res) -> { throw new BookNotFoundException(id); })
        .body(Book.class);
Note

RestTemplate still works but is in maintenance mode. New code uses RestClient (blocking) or WebClient (reactive).

HTTP service clients

New in Spring Boot 4: describe the remote API as an interface, let Spring implement it.

package com.example.clients;

@HttpExchange("/books")
public interface BookApi {

    @GetExchange("/{id}")
    Book byId(@PathVariable long id);

    @GetExchange
    List<Book> search(@RequestParam String author);

    @PostExchange
    Book create(@RequestBody BookRequest request);
}

Register the package and give the group a name:

@SpringBootApplication
@ImportHttpServices(group = "books", basePackages = "com.example.clients")
public class DemoApplication {  }

Configure the group instead of hard-coding URLs:

spring.http.clients.connect-timeout=1s
spring.http.serviceclient.books.base-url=https://api.example.com
spring.http.serviceclient.books.connect-timeout=2s
spring.http.serviceclient.books.read-timeout=2s

Then inject BookApi anywhere — no implementation class in your code base.

@Service
class Catalog {
    private final BookApi api;
    Catalog(BookApi api) { this.api = api; }

    Book fetch(long id) { return api.byId(id); }
}

Customize a group programmatically when properties are not enough:

@Bean
RestClientHttpServiceGroupConfigurer headers() {
    return groups -> groups.forEachClient((group, builder) ->
            builder.defaultHeader("X-Client", group.name()));
}

Timeouts

Always set them. A client without a read timeout turns a slow dependency into an outage.

spring.http.clients.connect-timeout=1s
spring.http.clients.read-timeout=5s

WebClient

Reactive and non-blocking; needs spring-boot-starter-webflux.

Mono<Book> book = webClient.get()
        .uri("/books/{id}", id)
        .retrieve()
        .bodyToMono(Book.class);

Use it for streaming responses or when you are already reactive.

Testing a client

@RestClientTest(Catalog.class)
class CatalogTest {

    @Autowired Catalog catalog;
    @Autowired MockRestServiceServer server;

    @Test
    void fetchesBook() {
        server.expect(requestTo("/books/1"))
              .andRespond(withSuccess("""
                  {"id":1,"title":"Dune"}""", MediaType.APPLICATION_JSON));

        assertThat(catalog.fetch(1).title()).isEqualTo("Dune");
    }
}

★ Exercises

  1. Call https://api.github.com/users/spring-projects with RestClient and print the name.
  2. Turn that call into an @HttpExchange interface and register it with @ImportHttpServices.
  3. Move the base URL into spring.http.serviceclient.<group>.base-url.
  4. Set a 2 s read timeout and point the client at a slow endpoint — what exception do you get?
  5. Map a 404 from the remote API to your own exception.