DEV Community

Cover image for Stop Writing REST Client Boilerplate in Java
Shrinivas Shukla
Shrinivas Shukla

Posted on Edited on

Stop Writing REST Client Boilerplate in Java

Every Java project ends up with the same file eventually. You know the one — ApiClient.java, HttpUtil.java, SomeServiceClient.java. It opens an HttpClient, builds a request, sets a few headers, fires it off, checks the status code by hand, and manually deserializes the response body. Then you copy-paste that same shape for the next endpoint. And the next.

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users/" + id))
    .header("Accept", "application/json")
    .GET()
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
    throw new RuntimeException("Request failed: " + response.statusCode());
}
User user = objectMapper.readValue(response.body(), User.class);
Enter fullscreen mode Exit fullscreen mode

Multiply that by every endpoint you call, add retry logic by hand because the network is unreliable, and you've got a few hundred lines of code that do nothing interesting.

I got tired of writing this, so I built REST-in-peace (RIP) — a declarative REST client for Java. You describe the API as an interface, and RIP gives you back a working implementation.

@RestClient
@BaseUrl("https://api.example.com")
public interface UserApi {

    @GET("/users/{id}")
    User getUser(@PathParam("id") String id);
}
Enter fullscreen mode Exit fullscreen mode
UserApi userApi = RIP.getClient(UserApi.class);
User user = userApi.getUser("42");
Enter fullscreen mode Exit fullscreen mode

That's the whole thing. No HttpClient setup, no manual JSON parsing, no status-code checking. Here's what it actually looks like running:

Declaring a UserApi interface and calling it with RIP.getClient

It's not just another Retrofit clone

If you've used Retrofit or Feign, the interface-based idea will feel familiar — annotate a method, get an HTTP call. The part I actually built RIP around is what happens underneath that interface.

Compile-time code generation, with a reflective fallback. Add the annotation processor and, for a supported interface, RIP implements it at compile time — you get a real generated class, not a runtime-built dynamic proxy. That means no reflection on your hot path, and no proxy-generation cost at startup. If the processor never ran, or the interface uses a shape it doesn't yet cover, RIP.getClient(...) transparently falls back to a reflective proxy that behaves identically.

GraalVM native-image just works. Because the generated implementation is real compiled bytecode instead of a runtime proxy, there's no reflect-config JSON to hand-write for RIP's own client-generation path. Build a native image the normal way and it works.

Resilience is built in, not bolted on. Retry with backoff and idempotency keys for safe retries on non-idempotent-looking calls are annotations right on the interface. A circuit breaker and a bulkhead are built in too, configured per client through RipClientConfig.Builder rather than a separate library you have to wire in yourself.

@RestClient
@BaseUrl("https://api.example.com")
public interface OrderApi {

    @GET("/orders/{id}")
    @Retry(times = 3)
    Order getOrder(@PathParam("id") String id);
}
Enter fullscreen mode Exit fullscreen mode

Response caching with stale-while-revalidate and negative caching, opt-in per client (or globally) by configuring a Cache, with @NoCache to exempt a single call.

A pluggable interceptor chain for logging, auth headers, request signing, or short-circuiting a call entirely (handy for feature flags or local overrides).

A Spring Boot starter, if that's your world — annotate the interface, add @EnableRestInPeaceClients to your configuration, and inject it like any other bean, zero hand-written @Bean methods.

Reactive support via rest-in-peace-reactor — any method returning Mono<T> or Flux<T> just works once you register the call adapter.

An OpenAPI-to-@RestClient generator, so if you already have a spec, you don't have to hand-write the interface at all.

A real local test server (MockRestServer) for integration-style tests, plus a JUnit 5 extension, so you're not mocking HttpClient directly.

How it compares

REST-in-peace vs. Retrofit, Feign/OpenFeign, and RestTemplate/WebClient

Try it

<dependency>
    <groupId>io.github.shrinivas93</groupId>
    <artifactId>rest-in-peace</artifactId>
    <version>1.0.0.63</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode
implementation("io.github.shrinivas93:rest-in-peace:1.0.0.63")
Enter fullscreen mode Exit fullscreen mode

It's on Maven Central, MIT licensed, and works from Java, Kotlin, or any other JVM language that can implement a Java interface.

If you try it and something's confusing, missing, or just annoying, I'd genuinely like to hear about it — open an issue or start a discussion.

Top comments (0)