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