Micronaut - HttpClient, Declarative Client and Filters
Call Superheroes via Command Center

I am a developer who loves Java, Spring, Quarkus, Micronaut, Open source, Microservices, Cloud
Search for a command to run...
Call Superheroes via Command Center

I am a developer who loves Java, Spring, Quarkus, Micronaut, Open source, Microservices, Cloud
No comments yet. Be the first to comment.
Micronaut is one of the promising java based frameworks in the Microservices space. Here is a series that I have started on Microservices framework based on Micronaut.
Help Superheroes to get discovered
Spring AI Tool Calling: From Chatbot to AI Agent with @Tool Your AI is smart. It knows an enormous amount. But it's frozen. It doesn't know what time it is right now. It doesn't know what's on your ca
Spring AI Advisors API Explained Series: Spring AI Complete Course — Lecture 4 of 12Reading Time: 8 minutesLevel: Intermediate Most developers stop at ChatClient. That is enough for demos. It is not
Working with Multiple AI Models in Spring AI Spring AI Complete Course — Lecture 3 of 12Previous: Lecture 2 — ChatClient API | Next: Lecture 4 — Advisors API In production AI applications, you rarel
Spring AI ChatClient API: The Fluent Heart of AI Integration Introduction If you've ever tried integrating AI models into a Java application, you know the pain. HTTP clients, API keys scattered everywhere, vendor-specific SDKs that never quite fit. W...
What is Spring AI? — Why Java Developers Need This in 2026 Every AI tutorial you see is in Python. LangChain, LlamaIndex, OpenAI SDK — all Python. But here's the uncomfortable truth: 80% of enterprise backends run Java. So who's building AI into thos...
In Microservices we will always have more than one service interacting with each other. One of the most popular ways for inter-service communication is REST API. While we are using POSTMAN to check our services as of now , in an actual service we require an agent to make REST calls for us. This is done by HttpClient in Micronaut.
If you are following the series, you must be aware that we have created the API for Superheroes and added the reactive version of API as well. We also checked at TestContainers to write efficient tests. In this section, we will look at calling the API from another service, let us call it command-center.
It will be the job of the command center to make calls to the services. It will help us to understand how API communication happens between two microservices. We will also add a Filter to add custom headers and track id as the header. In the end, we will update superhero API to check if the client who is making the request is valid using another HTTP Filter.
You can find the code of the article at
command-centercommand-center
We have changed the name as command-center, Build Tool as maven, Java version as 16 and the base package as com.codingsaint
Generate the new project and import it into your IDE (IntelliJ, eclipse)
Once our new project is imported into IDE we can add Superhero POJO (it's same as we did in superhero project) a Controller and a Service layer to our new project.
We will also create a class for keeping constant , let us name it CommandCenterConstants
public class CommandCenterConstants {
public static final String SUPERHERO_URL= "http://localhost:8080/";
public static final String USER_AGENT= "Coding Saint Command Center Client";
public static final String HEADER_ACCEPT= "application/json";
}
@Singleton
public class CommandCenterService {
private static final Logger logger = LoggerFactory.getLogger(CommandCenterService.class);
private final HttpClient client;
public CommandCenterService(@Client(SUPERHERO_URL) HttpClient client) {
this.client = client;
}
}
HttpClient to the Service layer. It will help us to do the Rest Calls to the Superhero API@Client annotation is has a value SUPERHERO_URL which is a constant from CommandCenterConstants and has a value http://localhost:8080/. Client annotation will let the HTTP client to connect to it while making a call.
@Controller
@ExecuteOn(TaskExecutors.IO)
public class CommandCenterController {
private static final Logger logger = LoggerFactory.getLogger(CommandCenterController.class);
private final CommandCenterService service;
public CommandCenterController(CommandCenterService commandCenterService) {
this.service = commandCenterService;
}
CommandCenterService injected into it. Let us call the endpoints of our Superhero through the service layer.
public Publisher<Superhero> create(Superhero superhero) {
logger.info("Adding a new saviour {} ", superhero);
var uri= UriBuilder.of("superhero").build();
HttpRequest req = HttpRequest.POST(uri, superhero)
.header("ACCEPT", "application/json");
var created = Mono.from( client.retrieve(req));
return created;
}
public Publisher<Superhero> update(Superhero superhero) {
logger.info("Updating the old saviour {} ", superhero);
var uri= UriBuilder.of("superhero").build();
HttpRequest req = HttpRequest.PUT(uri, superhero)
.header("ACCEPT", "application/json");
var updated = Mono.from( client.retrieve(req));
return updated;
}
public Publisher<Long> delete(Long id) {
logger.info("Purging the saviour {} ", id);
var uri= UriBuilder.of("superhero").path(String.valueOf(id)).build();
HttpRequest req = HttpRequest.DELETE(uri.toString())
.header("ACCEPT", "application/json");
return Mono.from( client.exchange(req));
}
variables and headers as required by the endpoint. Do note that the header is just to show that it can be added while invoking theURLs``` via HTTP client. We can also configure common Headers like Content-Type and even Authorization headers via Http Filter which we will add later.Now we will not directly call Superhero API but via command center. We will add endpoints to command center Controller , which will talk to Superhero via REST API using injected service layer.
Let us add methods for Controller as below
@Get("superhero/{id}")
public Mono<Superhero> superheroesById(Long id) {
return service.superheroesById(id);
}
@Post("/superhero")
Mono<Superhero> create(@Valid Superhero superhero) {
logger.info("Call to add a new saviour {} ", superhero);
return Mono.from(service.create(superhero));
}
@Put("superhero")
Mono<Superhero> update(@Valid Superhero superhero) {
return Mono.from(service.update(superhero));
}
@Delete("superhero/{id}")
Mono<HttpResponse<?>> delete(@NotNull Long id) {
return Mono
.from(service.delete(id))
.map(deleted -> deleted > 0 ? HttpResponse.noContent() : HttpResponse.notFound());
}
If you restart your server it will work. But I would like to show you HTTP request and response filters as well to control the API misuse and let only valid clients call Superhero API. While we will only show it as demo , you can enhance it to use production-ready service adding Authorization headers, or any other heder of your need.
Let us add the HTTP Request Filter in the command-center config.
@Filter( "/**")
public class SuperheroHttpClientFilter implements HttpClientFilter {
private static final Logger logger= LoggerFactory.getLogger(SuperheroHttpClientFilter.class);
@Override
public Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {
var header= request.getHeaders();
logger.debug("Adding HTTP Headers to client ");
header.add("User-Agent", "Coding Saint HTTP Client") ;
header.add("ACCEPT", "application/json");
String GUID= UUID.randomUUID().toString();
header.add("TRACKING_ID", GUID);
logger.info("Request with id {} , url {} , body {} to superheroes API ", GUID, request.getUri(), request.getBody());
return chain.proceed(request);
}
}
@Filter itself is telling them it's valid for all outgoing HTTP request with **doFilter method is adding User-Agent and a custom TRACKING-ID to the header of all outgoing requests.
``` We are logging the tracking id and URL to record the outgoing HTTP requests.We will start the command-center App at 8083. Let is update application.yml accordingly
micronaut:
application:
name: commandCenter
server:
port: 8083
Since we are adding it for all of the outgoing requests from command-center to Superhero API, let us add another filter to check if it's well-received or not. In case User-Agent and TRACKING_ID are not present in the header, let us simulate it as a bad request
We will add the following class at Superhero API
@Filter("/**")
public class SuperheroServerFilter implements HttpServerFilter {
private static final Logger logger = LoggerFactory.getLogger(SuperheroServerFilter.class);
@Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
String userAgent = request.getHeaders().get("User-Agent");
String trackingId = request.getHeaders().get("TRACKING_ID");
logger.info("Received requests from {} and Tracking id {} ", userAgent, trackingId);
if (StringUtils.isEmpty(userAgent) || StringUtils.isEmpty(trackingId)) {
var map = new HashMap<String, String>();
map.put("id", "Please provide a valid tracking id and user agent ");
return Publishers.just(HttpResponse.ok(map)
.status(HttpStatus.FORBIDDEN)
.contentType(MediaType.APPLICATION_JSON_TYPE));
}
return chain.proceed(request);
}
curl --location --request POST 'localhost:8080/superhero' \
--header 'User-Agent: Postman' \
--header 'TRACKING_ID: TEST_01' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Bug Killer",
"prefix": "Miss",
"suffix": "Bomber",
"power": "Bomb All the bugs in code"
}'
The response to the above request
{
"id": 1,
"name": "Bug Killer",
"prefix": "Miss",
"suffix": "Bomber",
"power": "Bomb All the bugs in code"
}
Do note that I have only shown POST endpoint . You can check all others similarly.
If you observe the logs , you can see that Filter logs are added in each request at the command-center app which is making an HTTP request and also is intercepted by API and validated.
06:08:24.857 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 892ms. Server Running: http://localhost:8083
06:08:34.869 [io-executor-thread-2] INFO c.c.CommandCenterController - Call to add a new saviour Superhero[id=null, name=Bug Killer, prefix=Miss, suffix=Bomber, power=Bomb All the bugs in code]
06:08:34.891 [io-executor-thread-2] INFO c.c.c.SuperheroHttpClientFilter - Request with id ee8c9d5e-d5b9-49df-be6b-b63c4bd9e9fe , url http://localhost:8080/superhero , body Optional[Superhero[id=null, name=Bug Killer, prefix=Miss, suffix=Bomber, power=Bomb All the bugs in code]] to superheroes API
06:14:32.524 [default-nioEventLoopGroup-1-2] INFO c.c.configs.SuperheroServerFilter - Received requests from Coding Saint HTTP Client and Tracking id 599cc7f7-781d-476f-9d5b-5c4c433b1905
06:14:32.830 [io-executor-thread-2] INFO com.codingsaint.SuperHeroController - Saving new Superhero Superhero[id=null, name=Bug Killer, prefix=Miss, suffix=Bomber, power=Bomb All the bugs in code]
We have written a low-level client with CommandCenterService, it gives us control before calling to Superhero API , but there are chances where you will not be interested in writing the full REST API calls.
We can use a declarative client which will be an interface. It will call the Superhero API as needed. Let us create SuperheroClient in the service layer.
Below is the code for the Declarative REST client with Micronaut.
@Client(SUPERHERO_URL)
@Header(name = "Accept", value = HEADER_ACCEPT)
public interface SuperheroClient {
@Get("/rx/superheroes")
Flux<Superhero> superheroes();
@Get("/rx/superhero/{id}")
Mono<Superhero> superheroesById(Long id);
@Post("/rx/superhero")
Publisher<Superhero> create( @Body Superhero superhero);
@Put("/rx/superhero")
Publisher<Superhero> update( Superhero superhero);
@Delete("/rx/superhero/{id}")
Publisher<HttpResponse<Long>> delete(Long id);
We can update the Controller and use any of the two i.e. CommandCenterService the low-level client or the SuperheroClient the declarative one.
@Get("superhero/{id}")
public Mono<Superhero> superheroesById(Long id) {
/* return service.superheroesById(id);*/
return client.superheroesById(id);
}
@Post("/superhero")
Mono<Superhero> create(@Valid Superhero superhero) {
logger.info("Call to add a new saviour {} ", superhero);
/* return Mono.from(service.create(superhero));*/
return Mono.from(client.create(superhero));
}
@Put("superhero")
Mono<Superhero> update(@Valid Superhero superhero) {
/* return Mono.from(service.update(superhero));*/
return Mono.from(client.update(superhero));
}
@Delete("superhero/{id}")
Mono<HttpResponse<?>> delete(@NotNull Long id) {
/* return Mono
.from(service.delete(id))
.map(deleted -> deleted > 0 ? HttpResponse.noContent() : HttpResponse.notFound());*/
return Mono
.from(client.delete(id));
}