REST Proxies
One of the most powerful features of the REST client class is the ability to turn a plain Java interface into a fully-working client for a 3rd-party REST resource.
See the guide: Call an existing REST API with a typed proxy
You describe the API with a Java interface and a handful of annotations — the HTTP method, the path, which arguments become headers, query parameters, form data, or the request body, and what the response should be parsed into. RestClient then hands you back a dynamic proxy that implements the interface, translating every method call into an HTTP request and every response back into a Java return value.
The methods used to retrieve remote interfaces are:
Quick Start
Define an interface, annotate it, and get a proxy from a RestClient:
@Remote(path="/petstore")
public interface PetStore {
@RemotePost("/pets")
Pet addPet(
@Content CreatePet createPet,
@Header("E-Tag") UUID etag,
@Query("debug") boolean debug
);
@RemoteGet("/pets/{petId}")
Pet getPet(@Path("petId") int petId);
}
// Use a RestClient with default JSON 5 support.
RestClient client = RestClient.create().json5().build();
// Instantiate our proxy interface.
PetStore store = client.getRemote(PetStore.class, "http://localhost:10000");
// Use it to create a pet...
CreatePet createPet = new CreatePet("Fluffy", 9.99);
Pet pet = store.addPet(createPet, UUID.randomUUID(), true);
// ...and fetch it back.
Pet sameCat = store.getPet(pet.getId());
The addPet(...) call above translates to a real HTTP request under the hood:
POST http://localhost:10000/petstore/pets HTTP/1.1
Accept: application/json
Content-Type: application/json
E-Tag: 475588d4-0b27-4f56-9296-cc683251d314
{
name: 'Fluffy',
price: 9.99
}
No hand-written URL building, serialization, or response parsing required — the interface is the contract.
Annotation Reference
Method behavior is driven entirely by annotations on the interface, its methods, and their parameters:
| Annotation | Applies to | Purpose |
|---|---|---|
@Remote | Interface | Marks the interface as a REST proxy; sets the base path, common headers, and client versioning. |
@RemoteOp (and @RemoteGet/@RemotePost/@RemotePut/@RemotePatch/@RemoteDelete) | Method | Maps a method to an HTTP verb + path. Often optional if the method name implies the verb. |
@Path | Parameter | URL path variables ({petId}-style placeholders). |
@Query | Parameter | Query-string parameters. |
@Header | Parameter | HTTP request headers — including dynamic, multi-value forms. |
@FormData | Parameter | application/x-www-form-urlencoded body parameters. |
@Content | Parameter | The HTTP request body. |
@Request | Parameter or class | Bundles multiple parts (content/headers/query/etc.) behind a single request bean. |
@Response | Return type | Bundles content/headers/status behind a single response bean. |
Every one of these supports both single-value forms (@Query("limit") int limit) and dynamic, multi-value
forms (@Query Map<String,Object> params) — see the dynamic headers example below
for the pattern in detail.
Capabilities Showcase
The sections below are short teasers of what's possible. Each links to a deep-dive page with the full set of supported types and edge cases.
Sending data to the server
Path variables (@Path) fill in {placeholders} in the URL:
@RemoteGet("/pets/{petId}")
Pet getPet(@Path("petId") int petId);
Query parameters (@Query) — and the equivalent for @FormData form posts:
@RemoteGet("/pets")
Pet[] findPets(@Query("species") String species, @Query("limit") int limit);
Dynamic and multiple headers
@Header (and @Query/@FormData/@Path) isn't limited to one header per parameter. Leave
the name off (or use the explicit wildcard "*") and pass a Map, a HeaderList, or a bean — every entry or
property is expanded into its own header:
@Remote(path="/petstore")
public interface PetStore {
// Every map entry becomes its own header.
@RemoteGet("/pets")
Pet[] getPets(@Header("*") Map<String,Object> headers);
// Same idea via a pre-built HeaderList...
@RemoteGet("/pets")
Pet[] getPetsViaHeaderList(@Header HeaderList headers);
// ...or a bean whose properties become individual headers.
@RemoteGet("/pets")
Pet[] getPetsViaBean(@Header PetHeaders headers);
}
Map<String,Object> headers = JsonMap.of("X-Client-Id", "acme", "X-Trace-Id", UUID.randomUUID());
Pet[] pets = store.getPets(headers);
This is a great way to forward a variable, caller-supplied set of headers (correlation IDs, feature flags, tenant context, etc.) without adding a new interface parameter every time one shows up. See @Header for the full list of supported dynamic forms.
Request bodies (@Content) accept any serializable POJO, or raw Reader/InputStream:
@RemotePost("/pets")
Pet addPet(@Content CreatePet pet);
Request beans (@Request) bundle several of the above behind one argument, so callers pass a single object instead of a long parameter list:
@RemotePost
String postPet(CreatePetRequest bean);
@Request
public class CreatePetRequest {
@Content
public CreatePet getContent() { ... }
@Query
public Map<String,Object> getQueryParams() { ... }
@Header("E-Tag")
public UUID getUUID() { ... }
}
Getting results back
Return types are flexible — parsed POJOs, raw streams, async wrappers, or just the status code:
@Remote(path="/petstore")
public interface PetStore {
@RemoteGet("/pets") Pet[] getPets(); // Parsed POJO
@RemoteGet(path="/pets", returns=STATUS) int getPetsStatus(); // Just the HTTP status
@RemoteGet("/pets") Future<Pet[]> getPetsAsync(); // Async
@RemoteGet("/pets/{petId}") InputStream getPetRaw(@Path("petId") int petId); // Raw stream
}
See @RemoteOp for the complete list of supported return types.
Response beans (@Response) do the same thing in reverse — bundle the body, headers, and status code of a response behind one typed object:
@RemotePost
CreatePetResponse postPet(@Content CreatePet pet);
@Response
public interface CreatePetResponse {
@Content Pet getContent();
@Header("E-Tag") UUID getUUID();
@StatusCode int getStatus();
}
Error handling happens automatically through checked exceptions. Declare a throws clause using any of the
roughly 60 predefined HTTP response exceptions
(or your own), and Juneau throws the matching one when the server returns that status code:
@RemoteGet("/pets/{petId}")
Pet getPet(@Path("petId") int petId) throws NotFound;
try {
Pet pet = store.getPet(999);
} catch (NotFound e) {
// HTTP 404 was returned - Juneau matched the status to the declared exception type.
}
Power-User Patterns
Default Values
Default values let you specify fallbacks for null arguments right on the annotation, so callers don't have to
pass every parameter every time:
@RemoteGet("/pets")
Pet[] getPets(
@Header(name="Accept-Language", def="en-US") String language,
@Query(name="limit", def="10") Integer limit
);
// Uses default language="en-US" and limit=10.
Pet[] pets = store.getPets(null, null);
def is supported on @Header, @Query, @FormData,
@Path, and @Content, at either the parameter or method level (parameter
wins when both are set).
Dual-Purpose Interfaces
The same Java interface can define both the client proxy and the server implementation, so a signature change is caught by the compiler on both sides instead of drifting apart at runtime:
@Remote(path="/petstore")
public interface PetStore {
@RemoteGet("/pet")
Collection<Pet> getPets() throws NotAcceptable;
}
@Rest(path="/petstore")
public class PetStoreResource extends BasicRestServlet implements PetStore {
@Override
@RestOp(method=GET, path="/pet")
public Collection<Pet> getPets() throws NotAcceptable {
return store.getPets();
}
}
See Dual-purpose (end-to-end) interfaces for the full walkthrough, including how
parameter-level annotations like @Header and @Path are inherited from the interface for free.
- @Remote - Interface-level configuration: base path, common headers, versioning.
- @RemoteOp - HTTP method/path mapping, naming-convention inference, return types.
- @Content - The request body.
- @FormData - Form post parameters.
- @Query - Query-string parameters.
- @Header - Request headers, including dynamic/multi-value forms.
- @Path - URL path variables.
- @Request - Bean-style bundling of request parts.
- @Response - Bean-style bundling of response parts.
- Dual-purpose (end-to-end) interfaces - Sharing one interface between client and server.