Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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.com

The 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

TaskCall
Send JSONrestClient.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.

Thanks for reading!

Donate to the site — we work hard for you.