Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

This 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 exist

The client works in tests but not in the application: check the scope of micronaut-http-client in pom.xml.

Thanks for reading!

Donate to the site — we work hard for you.