Java GraphQL Client
Usage¶
The DGS framework provides a GraphQL client that can be used to retrieve data from a GraphQL endpoint. The client is also useful for integration testing a DGS. The client has two components, each usable by itself, or in combination together.
- GraphQL Client - A HTTP client wrapper that provides easy parsing of GraphQL responses
- Query API codegen - Generate type-safe Query builders
The client has multiple interfaces and implementations for different needs. The interfaces are the following:
- GraphQLClient - Client interface for blocking implementations. Only recommended when using a blocking HTTP client.
- MonoGraphQLClient - The same as GraphQLClient, but based on the reactive
Mono
interface meant for non-blocking implementations. Comes with an out-of-the-box WebClient implementation:MonoGraphQLClient.createWithWebClient(...)
. - ReactiveGraphQLClient - Client interface for streaming responses such as Subscriptions, where multiple results are expected. Based on the reactive
Flux
interface. Implemented bySSESubscriptionGraphQLClient
andWebSocketGraphQLClient
.
GraphQL Client with WebClient¶
The easiest way to use the DGS GraphQL Client is to use the WebClient implementation.
WebClient is the recommended HTTP client in Spring, and is the best choice for most use cases, unless you have a specific reason to use a different HTTP client.
Because WebClient is Reactive, the client returns a Mono
for all operations.
//Configure a WebClient for your needs, e.g. including authentication headers and TLS.
WebClient webClient = WebClient.create("http://localhost:8080/graphql");
WebClientGraphQLClient client = MonoGraphQLClient.createWithWebClient(webClient);
//The GraphQLResponse contains data and errors.
Mono<GraphQLResponse> graphQLResponseMono = graphQLClient.reactiveExecuteQuery(query);
//GraphQLResponse has convenience methods to extract fields using JsonPath.
Mono<String> somefield = graphQLResponseMono.map(r -> r.extractValue("somefield"));
//Don't forget to subscribe! The request won't be executed otherwise.
somefield.subscribe();
//Configure a WebClient for your needs, e.g. including authentication headers and TLS.
val client = MonoGraphQLClient.createWithWebClient(WebClient.create("http://localhost:8080/graphql"))
//Executing the query returns a Mono of GraphQLResponse.
val result = client.reactiveExecuteQuery("{hello}").map { r -> r.extractValue<String>("hello") }
//Don't forget to subscribe! The request won't be executed otherwise.
somefield.subscribe();
The reactiveExecuteQuery
method takes a query String as input, and optionally a Map of variables and an operation name.
Instead of using a query String, you can use code generation to create a type-safe query builder API.
The GraphQLResponse
provides methods to parse and retrieve data and errors in a variety of ways.
Refer to the GraphQLResponse
JavaDoc for the complete list of supported methods.
method | description | example |
---|---|---|
getData | Get the data as a Map |
Map<String,Object> data = response.getData() |
dataAsObject | Parse data as the provided class, using the Jackson Object Mapper | TickResponse data = response.dataAsObject(TicksResponse.class) |
extractValue | Extract values given a JsonPath. The return type will be whatever type you expect, but depends on the JSON shape. For JSON objects, a Map is returned. Although this looks type safe, it really isn't. It's mostly useful for "simple" types like String, Int etc., and Lists of those types. | List<String> name = response.extractValue("movies[*].originalTitle") |
extractValueAsObject | Extract values given a JsonPath and deserialize into the given class | Ticks ticks = response.extractValueAsObject("ticks", Ticks.class) |
extractValueAsObject | Extract values given a JsonPath and deserialize into the given TypeRef. Useful for Maps and Lists of a certain class. | List<Route> routes = response.extractValueAsObject("ticks.edges[*].node.route", new TypeRef<List<Route>>(){}) |
getRequestDetails | Extract a RequestDetails object. This only works if requestDetails was requested in the query, and against the Gateway. |
RequestDetails requestDetails = response.getRequestDetails() |
getParsed | Get the parsed DocumentContext for further JsonPath processing |
response.getDocumentContext() |
The client can be used against any GraphQL endpoint (it doesn't have to be implemented with the DGS framework), but provides extra conveniences for parsing Gateway and DGS responses. This includes support for the Errors Spec.
Headers¶
HTTP headers can easily be added to the request.
WebClientGraphQLClient client = MonoGraphQLClient.createWithWebClient(webClient, headers -> headers.add("myheader", "test"));
By default, the client already sets the Content-type
and Accept
headers.
Errors¶
The GraphQLClient checks both for HTTP level errors (based on the response status code) and the errors
block in a GraphQL response.
The GraphQLClient is compatible with the Errors Spec used by the Gateway and DGS, and makes it easy to extract error information such as the ErrorType.
For example, for following GraphQL response the GraphQLClient lets you easily get the ErrorType and ErrorDetail fields.
Note that the ErrorType
is an enum as specified by the Errors Spec.
{
"errors": [
{
"message": "java.lang.RuntimeException: test",
"locations": [],
"path": [
"hello"
],
"extensions": {
"errorType": "BAD_REQUEST",
"errorDetail": "FIELD_NOT_FOUND"
}
}
],
"data": {
"hello": null
}
}
assertThat(graphQLResponse.errors.get(0).extensions.errorType).isEqualTo(ErrorType.BAD_REQUEST)
assertThat(graphQLResponse.errors.get(0).extensions.errorDetail).isEqualTo("FIELD_NOT_FOUND")
Plug in your own HTTP client¶
Instead of using WebClient, you can also plug in your own HTTP client. This is useful if you already have a configured client for your backend with authN/authZ, TLS, etc. In this case you are responsible for making the actual request, but the GraphQL client wraps the HTTP client and provides easy parsing of GraphQL responses.
There are two interfaces that you can pick from:
- GraphQLClient: For blocking HTTP clients
- MonoGraphQLClient: For non-blocking HTTP clients
Both interfaces return a GraphQLResponse
for each query execution, but MonoGraphQLClient
wraps the result in a Mono
, making it a better fit for non-blocking clients.
Create an instance by using the factory method on the interface.
This returns an instance of CustomGraphQLClient
or CustomMonoGraphQLClient
.
The implementations are named Custom*
to indicate you need to provide handling of the actual HTTP request.
CustomGraphQLClient client = GraphQLClient.createCustom(url, (url, headers, body) -> {
HttpHeaders httpHeaders = new HttpHeaders();
headers.forEach(httpHeaders::addAll);
ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(body, httpHeaders),String.class);
return new HttpResponse(exchange.getStatusCodeValue(), exchange.getBody());
});
GraphQLResponse graphQLResponse = client.executeQuery(query, emptyMap(), "SubmitReview");
String submittedBy = graphQLResponse.extractValueAsObject("submitReview.submittedBy", String.class);
//Configure your HTTP client
val restTemplate = RestTemplate();
val client = GraphQLClient.createCustom("http://localhost:8080/graphql") { url, headers, body ->
//Prepare the request, e.g. set up headers.
val httpHeaders = HttpHeaders()
headers.forEach { httpHeaders.addAll(it.key, it.value) }
//Use your HTTP client to send the request to the server.
val exchange = restTemplate.exchange(url, HttpMethod.POST, HttpEntity(body, httpHeaders), String::class.java)
//Transform the response into a HttpResponse
HttpResponse(exchange.statusCodeValue, exchange.body)
}
//Send a query and extract a value out of the result.
val result = client.executeQuery("{hello}").extractValue<String>("hello")
Alternatively, use MonoGraphQLClient.createCustomReactive(...)
to create the reactive equivalent.
The provided RequestExecutor
must now return Mono<HttpResponse>
.
CustomMonoGraphQLClient client = MonoGraphQLClient.createCustomReactive(url, (requestUrl, headers, body) -> {
HttpHeaders httpHeaders = new HttpHeaders();
headers.forEach(httpHeaders::addAll);
ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(body, httpHeaders),String.class);
return Mono.just(new HttpResponse(exchange.getStatusCodeValue(), exchange.getBody(), exchange.getHeaders()));
});
Mono<GraphQLResponse> graphQLResponse = client.reactiveExecuteQuery(query, emptyMap(), "SubmitReview");
String submittedBy = graphQLResponse.map(r -> r.extractValueAsObject("submitReview.submittedBy", String.class)).block();
Note that in this example we just use Mono.just
to create a Mono.
This doesn't make the call non-blocking.
Migrating from DefaultGraphQLClient¶
In previous versions of the framework we provided the DefaultGraphQLClient
class.
This has been deprecated for the following reasons:
- The "Default" in the name suggested that it should be the implementation for most use cases. However, the new WebClient implementation is a much better option now. Naming things is hard.
- The API required you to pass in the
RequestExecutor
for each query execution. This wasn't ergonomic for the new WebClient implementation, because noRequestExecutor
is required.
If you want to migrate existing usage of DefaultGraphQLClient
you can either use the WebClient implementation and get rid of your RequestExecutor
entirely, or alternatively use CustomGraphQLClient
/ CustomMonoGraphQLClient
which has almost the same API.
To migrate to CustomGraphQLClient
you pass in your existing RequestExecutor
to the GraphQLClient.createCustom(url, requestExecutor)
factory method, and remove it from the executeQuery
methods.
We plan to eventually remove the DefaultGraphQLClient
, because its API is confusing.
Type safe Query API¶
Based on a GraphQL schema a type safe query API can be generated for Java/Kotlin. The generated API is a builder style API that lets you build a GraphQL query, and it's projection (field selection). Because the code gets re-generated when the schema changes, it helps catch errors in the query. It's arguably also more readable, although multiline String support in Java and Kotlin do mitigate that issue as well.
If you own a DGS and want to generate a client for this DGS (e.g. for testing purposes) the client generation is just an extra property on the Codegen configuration.
Specify the following in your build.gradle
.
// Using plugins DSL
plugins {
id "com.netflix.dgs.codegen" version "[REPLACE_WITH_CODEGEN_PLUGIN_VERSION]"
}
generateJava{
packageName = 'com.example.packagename' // The package name to use to generate sources
generateClient = true
}
Code will be generated on build, and the generated code will be under builder/generated
, which is added to the classpath by the plugin.
With codegen configured correctly, a builder style API will be generated when building the project. Using the same query example as above, the query can be build using the generated builder API.
GraphQLQueryRequest graphQLQueryRequest =
new GraphQLQueryRequest(
new TicksGraphQLQuery.Builder()
.first(first)
.after(after)
.build(),
new TicksConnectionProjectionRoot()
.edges()
.node()
.date()
.route()
.name()
.votes()
.starRating()
.parent()
.grade());
String query = graphQLQueryRequest.serialize();
The GraphQLQueryRequest
is a class made available by the Codegen plugin, specifically the graphql-dgs-codegen-shared-core
module.
Such module will be added as an implementation dependency if the plugin is applied to the project.
The TicksGraphQLQuery
and TicksConnectionProjectionRoot
are generated.
After building the query, it can be serialized to a String, and executed using the GraphQLClient
.
Note that the edges
and node
fields are because the example schema is using Relay pagination.
Scalars in DGS Client¶
Custom scalars can be used in input types in GraphQL. Let's take the example of a DateTimeScalar
(created in Adding Custom Scalars).
In Java, we want to represent this as a LocalDateTime
class. When sending the query, we somehow have to serialize this.
There are many ways to represent a date, so how do we make sure that we use the same representation as the server expects?
In this release we added an optional scalars
argument to the GraphQLQueryRequest
constructor. This is a Map<Class<?>, Coercing<?,?>>
that maps the
Java class representing the input to an actual Scalar implementation. We will generate the query API with DateTimeScalar
as follows:
Map<Class<?>, Coercing<?, ?>> scalars = Map.of(LocalDateTime.class, DateTimeScalar.INSTANCE.getCoercing());
new GraphQLQueryRequest(
ReviewsGraphQLQuery.newRequest().dateRange(new DateRange(LocalDate.of(2020, 1, 1), LocalDate.now())).build(),
new ReviewsProjectionRoot().submittedDate().starScore(), scalars);
This way you can re-use exactly the same serialization code that you already have for your scalar implementation or one of the existing ones from - for example -
the graphql-dgs-extended-scalars
module.
Interface projections¶
When a field returns an interface, fields on the concrete types are specified using a fragment.
type Query @extends {
script(name: String): Script
}
interface Script {
title: String
director: String
actors: [Actor]
}
type MovieScript implements Script {
title: String
director: String
length: Int
}
type ShowScript implements Script {
title: String
director: String
episodes: Int
}
query {
script(name: "Top Secret") {
title
... on MovieScript {
length
}
}
}
This syntax is supported by the Query builder as well.
GraphQLQueryRequest graphQLQueryRequest =
new GraphQLQueryRequest(
new ScriptGraphQLQuery.Builder()
.name("Top Secret")
.build(),
new ScriptProjectionRoot()
.title()
.onMovieScript()
.length();
);
Building Federated Queries¶
You can use GraphQLQueryRequest
along with EntitiesGraphQLQuery
to generate federated queries.
The API provides a type-safe way to construct the _entities query with the associated representations
based on the input schema.
The representations
are passed in as a map of variables. Each representation class is generated based on the key
fields defined on the entity in your schema, along with the __typename
.
The EntitiesProjectionRoot
is used to select query fields on the specified type.
For example, let us look at a schema that extends a Movie
type:
type Movie @key(fields: "movieId") @extends {
movieId: Int @external
script: MovieScript
}
type MovieScript {
title: String
director: String
actors: [Actor]
}
type Actor {
name: String
gender: String
age: Int
}
With client code generation, you will now have a MovieRepresentation
containing the key field, i.e., movieId
, and the __typename
field already set to type Movie
.
Now you can add each representation to the EntitiesGraphQLQuery
as a representations
variable.
You will also have a EntitiesProjectionRoot
with onMovie()
to select fields on Movie
from.
Finally, you put them all together as a GraphQLQueryRequest
, which you serialize into the final query string.
The map of representations
variables is available via getVariables
on the EntitiesGraphQLQuery
.
Here is an example for the schema shown earlier:
EntitiesGraphQLQuery entitiesQuery = new EntitiesGraphQLQuery.Builder()
.addRepresentationAsVariable(
MovieRepresentation.newBuilder().movieId(1122).build()
)
.build();
GraphQLQueryRequest request = new GraphQLQueryRequest(
entitiesQuery,
new EntitiesProjectionRoot().onMovie().movieId().script().title()
);
String query = request.serialize();
Map<String, Object> representations = entitiesQuery.getVariables();
Subscriptions¶
Subscriptions are supported through the ReactiveGraphQLClient
interface.
The interface has two implementations:
WebSocketGraphQLClient
: For subscriptions over WebSocketsSSESubscriptionGraphQLClient
: For subscriptions over Server Sent Events (SSE)
Both implementations require the use of WebClient, and cannot be used with other HTTP clients (in contrast to the "normal" DGS client).
The clients return a Flux
of GraphQLResponse
.
Each GraphQLResponse
represents a message pushed from the subscription, and contains data
and errors
.
It also offers convenience methods to parse data using JsonPath.
WebClient webClient = WebClient.create("http://localhost:" + port);
SSESubscriptionGraphQLClient client = new SSESubscriptionGraphQLClient("/subscriptions", webClient);
Flux<GraphQLResponse> numbers = client.reactiveExecuteQuery("subscription {numbers}", Collections.emptyMap());
numbers
.mapNotNull(r -> r.extractValue("data.numbers"))
.log()
.subscribe();
val webClient = WebClient.create("http://localhost:$port")
val client = SSESubscriptionGraphQLClient("/subscriptions", webClient)
val reactiveExecuteQuery = client.reactiveExecuteQuery("subscription {numbers}", emptyMap())
reactiveExecuteQuery
.mapNotNull { r -> r.data["numbers"] }
.log()
.subscribe()
In case the connection fails to set up, either because of a connection error, or because of an invalid query, a WebClientResponseException
will be thrown.
Errors later on in the process will be errors in the stream.
Don't forget to subscribe()
to the stream, otherwise the connection doesn't get started!
An example of using the client to write subscription integration tests is available here.