Declarative HTTP client
Microservices call each other over HTTP all the time. In Micronaut you do not have to build the request and parse the JSON by hand: you describe an interface with annotations, just like a controller, and the annotation processor generates the implementation. Such a client is called declarative: you declare what you need instead of writing how to do it.
As an example, the blog will import articles from the practice API jsonplaceholder.typicode.com. It has a /posts/{id} address that returns a post as JSON.
The dependency
The generator already added micronaut-http-client, but with <scope>test</scope>, for tests only. For the client to work in the application itself, change the scope in pom.xml:
<!-- pom.xml -->
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-client</artifactId>
<scope>compile</scope>
</dependency>The model and the client
A record for the other service's response:
// src/main/java/com/example/ExternalPost.java
package com.example;
import io.micronaut.serde.annotation.Serdeable;
@Serdeable
public record ExternalPost(Long id, Long userId, String title, String body) {
}The client interface:
// src/main/java/com/example/PlaceholderClient.java
package com.example;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client;
import java.util.List;
import java.util.Optional;
@Client(id = "placeholder")
public interface PlaceholderClient {
@Get("/posts/{id}")
Optional<ExternalPost> fetchPost(Long id);
@Get("/posts?userId={userId}")
List<ExternalPost> fetchByUser(Long userId);
}id = "placeholder" is the service name. Its address goes into the settings, not into the code:
# src/main/resources/application.properties
micronaut.http.services.placeholder.url=https://jsonplaceholder.typicode.comThis makes the address easy to change for tests or the server. You can also give the address directly: @Client("https://jsonplaceholder.typicode.com").
Using it
The client is an ordinary bean, injected through the constructor:
// src/main/java/com/example/ImportController.java
package com.example;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.exceptions.HttpStatusException;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
@ExecuteOn(TaskExecutors.BLOCKING)
@Controller("/import")
public class ImportController {
private final PlaceholderClient placeholderClient;
private final ArticleService articleService;
public ImportController(PlaceholderClient placeholderClient, ArticleService articleService) {
this.placeholderClient = placeholderClient;
this.articleService = articleService;
}
@Post("/{id}")
public Article importPost(Long id) {
ExternalPost post = placeholderClient.fetchPost(id)
.orElseThrow(() -> new HttpStatusException(HttpStatus.NOT_FOUND, "No post " + id + " on the remote server"));
return articleService.create(new ArticleForm(post.title(), post.body()));
}
}Calling the client blocks the thread until the answer arrives, so the controller is again marked @ExecuteOn(TaskExecutors.BLOCKING).
Trying it out
curl -X POST http://localhost:8080/import/2{"id":1,"title":"qui est esse","body":"est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\n..."}The post from the remote server was saved as a blog article. For a post that does not exist:
curl -X POST http://localhost:8080/import/1000{"_links":{"self":[{"href":"/import/1000","templated":false}]},"_embedded":{"errors":[{"message":"No post 1000 on the remote server"}]},"message":"Not Found"}When a client method returns an Optional, a 404 response becomes an empty Optional. Other errors (500, 401) are thrown by the client as HttpClientResponseException.
Common mistake
If you leave the dependency with <scope>test</scope>, the main code does not compile:
[ERROR] PlaceholderClient.java:[4,43] package io.micronaut.http.client.annotation does not existThe client works in tests but not in the application: check the scope of micronaut-http-client in pom.xml.