Calling other APIs: RestClient
Sometimes the server itself needs to call another API: get exchange rates, check an address, fetch data. In the tutorial you made such requests with java.net.http.HttpClient. Spring has RestClient for this — a fluent client that turns response JSON into a Java object itself. Let's teach the blog to import a post from the test API JSONPlaceholder.
Setup
In Spring Boot 4 the client is a separate starter (on start.spring.io — HTTP Client):
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-restclient</artifactId>
</dependency>The other API's address goes into the settings:
# src/main/resources/application.yaml
blog:
import:
base-url: https://jsonplaceholder.typicode.comThe other API's response
The request https://jsonplaceholder.typicode.com/posts/1 returns JSON with the fields userId, id, title and body. Let's describe a record with only the fields we need — Spring skips the rest:
// src/main/java/com/example/blog/ExternalPost.java
package com.example.blog;
public record ExternalPost(Long id, String title, String body) {
}The import service
// src/main/java/com/example/blog/ImportService.java
package com.example.blog;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatusCode;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
@Service
public class ImportService {
private final RestClient restClient;
private final PostService postService;
public ImportService(RestClient.Builder builder,
@Value("${blog.import.base-url}") String baseUrl,
PostService postService) {
this.restClient = builder.baseUrl(baseUrl).build();
this.postService = postService;
}
public PostResponse importPost(Long externalId, String username) {
ExternalPost external = restClient.get()
.uri("/posts/{id}", externalId)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
throw new IllegalArgumentException("No external post " + externalId);
})
.body(ExternalPost.class);
var request = new NewPostRequest(external.title(), external.body());
return postService.create(request, username);
}
}Spring Boot creates the RestClient.Builder itself, with Jackson already configured in it. uri("/posts/{id}", externalId) fills the id into the address, retrieve() runs the request, body(ExternalPost.class) turns the JSON into the record. onStatus catches 4xx responses and throws our exception — the handler from the error chapter turns it into a 400.
The import address
// src/main/java/com/example/blog/ImportController.java
@PostMapping("/api/import/{externalId}")
@ResponseStatus(HttpStatus.CREATED)
public PostResponse importPost(@PathVariable Long externalId,
Principal principal) {
return importService.importPost(externalId, principal.getName());
}Only signed-in users can import, so the request carries the token from the JWT lesson:
curl -X POST http://localhost:8080/api/import/1 -H "Authorization: Bearer $TOKEN"
curl -X POST http://localhost:8080/api/import/9999 -H "Authorization: Bearer $TOKEN"{"id":65,"title":"sunt aut facere repellat provident occaecati excepturi optio reprehenderit","body":"quia et suscipit\nsuscipit recusandae ...","author":"anna","createdAt":"2026-10-04T09:10:31.243724400Z"}
{"detail":"No external post 9999","instance":"/api/import/9999","status":400,"title":"Bad Request"}The first post came from JSONPlaceholder (its texts are Latin placeholders) and was saved in the blog in Anna's name. For a missing id the other API answered 404, and we returned a clear message to the client.
Other calls
| Task | Call |
|---|---|
| Send JSON | restClient.post().uri("/posts").body(request).retrieve() |
| A header | .header("Authorization", "Bearer ...") |
| A response with code and headers | .retrieve().toEntity(ExternalPost.class) |
| A list | .body(new ParameterizedTypeReference<List<ExternalPost>>() {}) |
Common mistake
Without the spring-boot-starter-restclient starter the application does not start:
No qualifying bean of type 'org.springframework.web.client.RestClient$Builder' available: expected at least 1 bean which qualifies as autowire candidate.In Spring Boot 3 the client came with the web starter; in Spring Boot 4 it is added separately. Add the dependency — and the RestClient.Builder bean appears.