A Java code-first type-safe GraphQL Client API suggestion for [Microprofile GraphQL](microprofile/microprofile-graphql#185).
Basic Usage:
@GraphQlClientApi
interface SuperHeroesApi {
List<SuperHero> allHeroesIn(String location); // (1)
}
class SuperHero {
private String name;
private List<String> superPowers;
}
class MyApplication {
@Inject SuperHeroesApi superHeroesApi;
List<SuperHero> allHeroes = superHeroesApi.allHeroesIn("Outer Space");
}
-
The default request type is
query
. To make it a mutation, annotate it@Mutation
. The parameter name is only available if you compile the source with the-parameters
option. Otherwise, you’ll have to annotate all parameters with@Name
.
The example above uses CDI, e.g. when you are in a MicroProfile or Jakarta EE environment. If you are in an environment without CDI support, you need to instantiate the API interface by using the builder:
SuperHeroesApi api = GraphQlClientBuilder.newBuilder().build(SuperHeroesApi.class);
The basic idea of the Java code-first approach is that you start by writing the DTOs and query/mutation methods as you need them in your client. This ensures that you don’t request fields that you don’t need; the thinking is inspired by Consumer Driven Contracts and may evolve more into that direction. If the server uses different names, use annotations to do a mapping:
If the server defines a different field or parameter name, annotate it with @Name
.
If the server defines a different query name, annotate the method as, e.g., @Query("findHeroesCurrentlyLocatedIn")
.
By renaming methods, you can also define several variations of the same request but using different return types or parameters. E.g.:
public interface SuperHeroesApi {
SuperHero findHeroByName(String name); // (1)
@Query("findHeroByName")
SuperHeroWithTeams findHeroWithTeamsByName(String name); // (2)
}
-
The
SuperHero
class has no team affiliations (for this example). -
The
SuperHeroWithTeams
class has aprivate List<Team> teamAffiliations
field. TheTeam
class doesn’t contain the members to break recursion.
If the endpoint is always the same, e.g. a public API of a cloud service, you can add the URL to your API annotation, e.g.:
@GraphQlClientApi(endpoint = "https://superheroes.org/graphql")
interface SuperHeroesApi {
}
When instantiating the API with the builder, you can set (or overwrite) the endpoint there:
SuperHeroesApi api = GraphQlClientBuilder.newBuilder()
.endpoint("https://superheroes.org/graphql")
.build(SuperHeroesApi.class);
Commonly you’ll need different endpoints, e.g. when you need one endpoint for your production system, but a different endpoint for your test system. Simply use MicroProfile Config to set the endpoint; similar to the MicroProfile Rest Client, the key for the endpoint is the fully qualified name of the api interface plus /mp-graphql/url
, e.g.:
org.superheroes.SuperHeroesApi/mp-graphql/url=https://superheroes.org/graphql
If you want to use a different key, set the base config key on the annotation @GraphQlClientApi(configKey = "superheroes")
; then use this key for the endpoint superheroes/mp-graphql/url
.
When using the builder, you can override the config key there as well: GraphQlClientBuilder.newBuilder().configKey("superheroes")
.
To add a custom header to the http requests sent out by the GraphQL Client, annotate your method or the API interface as @Header
, e.g.:
@GraphQlClientApi
interface SuperHeroesApi {
@Header(name = "S.H.I.E.L.D.-Clearance", constant = "TOP-SECRET")
List<SuperHero> allHeroesIn(String location);
}
The name
is always fixed, but the value can also be the name of a method for dynamic values, e.g.:
@GraphQlClientApi
interface SuperHeroesApi {
@Header(name = "S.H.I.E.L.D.-Clearance", method = "establishShieldClearance")
List<SuperHero> allHeroesIn(String location);
static Clearance establishShieldClearance() { // (1)
return userIsInRole(MANAGER) ? TOP_SECRET : PUBLIC;
}
}
-
This value is an enum, but it can be any Object; the GraphQL client calls <code>toString</code> to convert it.
The method must be static
and accessible by the interface, i.e. in the interface itself or in one of the classes it’s nested in; if it’s in a different class, prefix it with the fully qualified class name and a dot "."
, e.g. @Header(name = "S.H.I.E.L.D.-Clearance", method = "org.superheroes.SecurityTools.establishShieldClearance")
.
In rare cases, you may want to pass the value of a header as a parameter:
@GraphQlClientApi
interface SuperHeroesApi {
List<SuperHero> allHeroesIn(String location, @Header(name = "S.H.I.E.L.D.-Clearance") Clearance clearance);
}
A @Header
parameter is not part of the GraphQL request.
@Header
annotations can be defined via `@Stereotype`s.
To add an Authorization
header, instead of using the generic @Header
annotation, you can also use the special @AuthorizationHeader
annotation. It produces a BASIC
Authorization
header by default or a BEARER
token. You can configure the credentials in MP Config with a prefix plus /mp-graphql/
and either username
and password
for BASIC
or bearer
for BEARER
. The config key defaults to the fully qualified name of the GraphQlClientApi
interface or its configKey
.
You can use a custom prefix by setting the confPrefix
. The infix /mp-graphql/
is still applied, unless you end the confPrefix
with , e.g.
@AuthorizationHeader(confPrefix = "org.superheroes.security.basic.
will use
org.superheroes.security.basic.username
, while *
will use plain username
.
@AuthorizationHeader
annotations can be defined via `@Stereotype`s.