Files
spring-graphql/spring-graphql-docs/modules/ROOT/pages/client.adoc
Brian Clozel 70f16c1158 Configure JSON GraphQlModule in client support
The various GraphQL clients supported in this project automatically detect
JSON codecs for reading/writing GraphQL requests as JSON payloads.
If there is none detected, clients provide a default codec instance.

This commit configures automatically the `GraphQlModule` from gh-1174 in
default codecs and add integration tests for `FieldValue<T>` usage on the
client side.

This also improves the documentation around `FieldValue<T>` for both
server and client side support.

Closes gh-1190
2025-04-15 11:44:05 +02:00

478 lines
16 KiB
Plaintext

[[client]]
= Client
Spring for GraphQL includes client support to execute GraphQL requests over HTTP,
WebSocket, and RSocket.
[[client.graphqlclient]]
== `GraphQlClient`
`GraphQlClient` defines a common workflow for GraphQL requests independent of the underlying
transport, so the way you perform requests is the same no matter what transport is in use.
The following transport specific `GraphQlClient` extensions are available:
- xref:client.adoc#client.httpsyncgraphqlclient[HttpSyncGraphQlClient]
- xref:client.adoc#client.httpgraphqlclient[HttpGraphQlClient]
- xref:client.adoc#client.websocketgraphqlclient[WebSocketGraphQlClient]
- xref:client.adoc#client.rsocketgraphqlclient[RSocketGraphQlClient]
Each defines a `Builder` with options relevant to the transport. All builders extend
from a common, base GraphQlClient xref:client.adoc#client.graphqlclient.builder[`Builder`]
with options applicable to all transports.
Once `GraphQlClient` is built you can begin to make xref:client.adoc#client.requests[requests].
Typically, the GraphQL operation for a request is provided as text. Alternatively, you
can use https://github.com/Netflix/dgs-codegen[DGS Codegen] client API classes through
xref:client.adoc#client.dgsgraphqlclient[DgsGraphQlClient], which can wrap any of the
above `GraphQlClient` extensions.
[[client.httpsyncgraphqlclient]]
=== HTTP Sync
`HttpSyncGraphQlClient` uses
{spring-framework-ref-docs}/integration/rest-clients.html#rest-restclient[RestClient]
to execute GraphQL requests over HTTP through a blocking transport contract and chain of
interceptors.
include-code::SyncClientUsage[tag=create,indent=0]
Once `HttpSyncGraphQlClient` is created, you can begin to
xref:client.adoc#client.requests[execute requests] using the same API, independent of the underlying
transport. If you need to change any transport specific details, use `mutate()` on an
existing `HttpSyncGraphQlClient` to create a new instance with customized settings:
include-code::SyncClientUsage[tag=mutate,indent=0]
[[client.httpgraphqlclient]]
=== HTTP
`HttpGraphQlClient` uses
{spring-framework-ref-docs}/web/webflux-webclient.html[WebClient] to execute
GraphQL requests over HTTP through a non-blocking transport contract and chain of
interceptors.
include-code::ClientUsage[tag=create,indent=0]
Once `HttpGraphQlClient` is created, you can begin to
xref:client.adoc#client.requests[execute requests] using the same API, independent of the underlying
transport. If you need to change any transport specific details, use `mutate()` on an
existing `HttpGraphQlClient` to create a new instance with customized settings:
include-code::ClientUsage[tag=mutate,indent=0]
[[client.websocketgraphqlclient]]
=== WebSocket
`WebSocketGraphQlClient` executes GraphQL requests over a shared WebSocket connection.
It is built using the
{spring-framework-ref-docs}/web/webflux-websocket.html#webflux-websocket-client[WebSocketClient]
from Spring WebFlux and you can create it as follows:
include-code::WebSocketClientUsage[tag=create,indent=0]
In contrast to `HttpGraphQlClient`, the `WebSocketGraphQlClient` is connection oriented,
which means it needs to establish a connection before making any requests. As you begin
to make requests, the connection is established transparently. Alternatively, use the
client's `start()` method to establish the connection explicitly before any requests.
In addition to being connection-oriented, `WebSocketGraphQlClient` is also multiplexed.
It maintains a single, shared connection for all requests. If the connection is lost,
it is re-established on the next request or if `start()` is called again. You can also
use the client's `stop()` method which cancels in-progress requests, closes the
connection, and rejects new requests.
TIP: Use a single `WebSocketGraphQlClient` instance for each server in order to have a
single, shared connection for all requests to that server. Each client instance
establishes its own connection and that is typically not the intent for a single server.
Once `WebSocketGraphQlClient` is created, you can begin to
xref:client.adoc#client.requests[execute requests] using the same API, independent of the underlying
transport. If you need to change any transport specific details, use `mutate()` on an
existing `WebSocketGraphQlClient` to create a new instance with customized settings:
include-code::WebSocketClientUsage[tag=mutate,indent=0]
`WebSocketGraphQlClient` supports sending periodic ping messages to keep the connection
active when no other messages are sent or received. You can enable that as follows:
include-code::WebSocketClientUsage[tag=keepAlive,indent=0]
[[client.websocketgraphqlclient.interceptor]]
==== Interceptor
The https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md[GraphQL over WebSocket]
protocol defines a number of connection oriented messages in addition to executing
requests. For example, a client sends `"connection_init"` and the server responds with
`"connection_ack"` at the start of a connection.
For WebSocket transport specific interception, you can create a
`WebSocketGraphQlClientInterceptor`:
[source,java,indent=0,subs="verbatim,quotes"]
----
static class MyInterceptor implements WebSocketGraphQlClientInterceptor {
@Override
public Mono<Object> connectionInitPayload() {
// ... the "connection_init" payload to send
}
@Override
public Mono<Void> handleConnectionAck(Map<String, Object> ackPayload) {
// ... the "connection_ack" payload received
}
}
----
xref:client.adoc#client.interception[Register] the above interceptor as any other
`GraphQlClientInterceptor` and use it also to intercept GraphQL requests, but note there
can be at most one interceptor of type `WebSocketGraphQlClientInterceptor`.
[[client.rsocketgraphqlclient]]
=== RSocket
`RSocketGraphQlClient` uses
{spring-framework-ref-docs}/rsocket.html#rsocket-requester[RSocketRequester]
to execute GraphQL requests over RSocket requests.
include-code::RSocketClientUsage[tag=create,indent=0]
In contrast to `HttpGraphQlClient`, the `RSocketGraphQlClient` is connection oriented,
which means it needs to establish a session before making any requests. As you begin
to make requests, the session is established transparently. Alternatively, use the
client's `start()` method to establish the session explicitly before any requests.
`RSocketGraphQlClient` is also multiplexed. It maintains a single, shared session for
all requests. If the session is lost, it is re-established on the next request or if
`start()` is called again. You can also use the client's `stop()` method which cancels
in-progress requests, closes the session, and rejects new requests.
TIP: Use a single `RSocketGraphQlClient` instance for each server in order to have a
single, shared session for all requests to that server. Each client instance
establishes its own connection and that is typically not the intent for a single server.
Once `RSocketGraphQlClient` is created, you can begin to
xref:client.adoc#client.requests[execute requests] using the same API, independent of the underlying
transport.
[[client.graphqlclient.builder]]
=== Builder
`GraphQlClient` defines a parent `BaseBuilder` with common configuration options for the
builders of all extensions. Currently, it has lets you configure:
- `DocumentSource` strategy to load the document for a request from a file
- xref:client.adoc#client.interception[Interception] of executed requests
`BaseBuilder` is further extended by the following:
- `SyncBuilder` - blocking execution stack with a chain of ``SyncGraphQlInterceptor``'s.
- `Builder` - non-blocking execution stack with chain of ``GraphQlInterceptor``'s.
[[client.requests]]
== Requests
Once you have a xref:client.adoc#client.graphqlclient[`GraphQlClient`], you can begin to perform requests via
xref:client.adoc#client.requests.retrieve[retrieve] or xref:client.adoc#client.requests.execute[execute]
methods.
[[client.requests.retrieve]]
=== Retrieve
The below retrieves and decodes the data for a query:
[tabs]
======
Sync::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
include::{include-java}/client/requests/retrieve/Retrieve.java[tag=retrieveSync,indent=0]
----
Non-Blocking::
+
[source,java,indent=0,subs="verbatim,quotes",role="secondary"]
----
include::{include-java}/client/requests/retrieve/Retrieve.java[tag=retrieve,indent=0]
----
======
<1> The operation to perform.
<2> The path under the "data" key in the response map to decode from.
<3> Decode the data at the path to the target type.
The input document is a `String` that could be a literal or produced through a code
generated request object. You can also define documents in files and use a
xref:client.adoc#client.requests.document-source[Document Source] to resole them by file name.
The path is relative to the "data" key and uses a simple dot (".") separated notation
for nested fields with optional array indices for list elements, e.g. `"project.name"`
or `"project.releases[0].version"`.
Decoding can result in `FieldAccessException` if the given path is not present, or the
field value is `null` and has an error. `FieldAccessException` provides access to the
response and the field:
[tabs]
======
Sync::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
include::{include-java}/client/requests/retrieve/Retrieve.java[tag=fieldErrorSync,indent=0]
----
Non-Blocking::
+
[source,java,indent=0,subs="verbatim,quotes",role="secondary"]
----
include::{include-java}/client/requests/retrieve/Retrieve.java[tag=fieldError,indent=0]
----
======
[[client.requests.execute]]
=== Execute
xref:client.adoc#client.requests.retrieve[Retrieve] is only a shortcut to decode from a single path in the
response map. For more control, use the `execute` method and handle the response:
For example:
[tabs]
======
Sync::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
include::{include-java}/client/requests/execute/Execute.java[tag=executeSync,indent=0]
----
Non-Blocking::
+
[source,java,indent=0,subs="verbatim,quotes",role="secondary"]
----
include::{include-java}/client/requests/execute/Execute.java[tag=execute,indent=0]
----
======
<1> The response does not have data, only errors
<2> Field that was set to `null` by its `DataFetcher`
<3> Field that is `null` and has an associated error
<4> Decode the data at the given path
[[client.requests.document-source]]
=== Document Source
The document for a request is a `String` that may be defined in a local variable or
constant, or it may be produced through a code generated request object.
You can also create document files with extensions `.graphql` or `.gql` under
`"graphql-documents/"` on the classpath and refer to them by file name.
For example, given a file called `projectReleases.graphql` in
`src/main/resources/graphql-documents`, with content:
[source,graphql,indent=0,subs="verbatim,quotes"]
.src/main/resources/graphql-documents/projectReleases.graphql
----
query projectReleases($slug: ID!) {
project(slug: $slug) {
name
releases {
version
}
}
}
----
You can then:
[source,java,indent=0,subs="verbatim,quotes"]
----
include::{include-java}/client/requests/documentsource/DocumentSource.java[tag=documentSource,indent=0]
----
<1> Load the document from "projectReleases.graphql"
<2> Provide variable values.
The "JS GraphQL" plugin for IntelliJ supports GraphQL query files with code completion.
You can use the `GraphQlClient` xref:client.adoc#client.graphqlclient.builder[Builder] to customize the
`DocumentSource` for loading documents by names.
[[client.subscriptions]]
== Subscription Requests
Subscription requests require a client transport that is capable of streaming data.
You will need to create a `GraphQlClient` that support this:
- xref:client.adoc#client.httpgraphqlclient[HttpGraphQlClient] with Server-Sent Events
- xref:client.adoc#client.websocketgraphqlclient[WebSocketGraphQlClient] with WebSocket
- xref:client.adoc#client.rsocketgraphqlclient[RSocketGraphQlClient] with RSocket
[[client.subscriptions.retrieve]]
=== Retrieve
To start a subscription stream, use `retrieveSubscription` which is similar to
xref:client.adoc#client.requests.retrieve[retrieve] for a single response but returning a stream of
responses, each decoded to some data:
include-code::RetrieveSubscription[tag=subscriptionRetrieve,indent=0]
The `Flux` may terminate with `SubscriptionErrorException` if the subscription ends from
the server side with an "error" message. The exception provides access to GraphQL errors
decoded from the "error" message.
The `Flux` may terminate with `GraphQlTransportException` such as
`WebSocketDisconnectedException` if the underlying connection is closed or lost. In that
case you can use the `retry` operator to restart the subscription.
To end the subscription from the client side, the `Flux` must be cancelled, and in turn
the WebSocket transport sends a "complete" message to the server. How to cancel the
`Flux` depends on how it is used. Some operators such as `take` or `timeout` themselves
cancel the `Flux`. If you subscribe to the `Flux` with a `Subscriber`, you can get a
reference to the `Subscription` and cancel through it. The `onSubscribe` operator also
provides access to the `Subscription`.
[[client.subscriptions.execute]]
=== Execute
xref:client.adoc#client.subscriptions.retrieve[Retrieve] is only a shortcut to decode from a single path in each
response map. For more control, use the `executeSubscription` method and handle each
response directly:
include-code::ExecuteSubscription[tag=subscriptionExecute,indent=0]
[[client.interception]]
== Interception
For blocking transports created with the `GraphQlClient.SyncBuilder`, you create a
`SyncGraphQlClientInterceptor` to intercept all requests through the client:
include-code::SyncInterceptor[]
For non-blocking transports created with `GraphQlClient.Builder`, you create a
`GraphQlClientInterceptor` to intercept all requests through the client:
include-code::MyInterceptor[]
Once the interceptor is created, register it through the client builder. For example:
include-code::UseInterceptor[tag=register,indent=0]
[[client.fieldvalue]]
== `FieldValue`
By default, input types in GraphQL are nullable and optional, an input value (or any of its fields)
can be set to the `null` literal, or not provided at all. This distinction is useful for
partial updates with a mutation where the underlying data may also be, either set to
`null` or not changed at all accordingly.
Similar to the xref:controllers.adoc#controllers.schema-mapping.fieldvalue[`FieldValue<T> support in controllers`],
we can wrap an Input type with `FieldValue<T>` or use it at the level of class attributes on the client side.
Given a `ProjectInput` class like:
include-code::ProjectInput[indent=0]
We can use our client to send a mutation request:
include-code::FieldValueClient[tag=fieldvalue,indent=0]
For this to work, the client must use Jackson for JSON (de)serialization and must be configured
with the `org.springframework.graphql.client.json.GraphQlModule`.
This can be registered manually on the underlying HTTP client like so:
include-code::FieldValueClient[tag=createclient,indent=0]
This `GraphQlModule` can be globally registered in Spring Boot applications by contributing it as a bean:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
public class GraphQlJsonConfiguration {
@Bean
public GraphQLModule graphQLModule() {
return new GraphQLModule();
}
}
----
[[client.dgsgraphqlclient]]
== DGS Codegen
As an alternative to providing the operation such as a mutation, query, or subscription as
text, you can use the https://github.com/Netflix/dgs-codegen[DGS Codegen] library to
generate client API classes that let you use a fluent API to define the request.
Spring for GraphQL provides xref:client.adoc#client.dgsgraphqlclient[DgsGraphQlClient]
that wraps any `GraphQlClient` and helps to prepare the request with generated client
API classes.
For example, given the following schema:
[source,graphql,indent=0,subs="verbatim,quotes"]
----
type Query {
books: [Book]
}
type Book {
id: ID
name: String
}
----
You can perform a request as follows:
[source,java,indent=0,subs="verbatim,quotes"]
----
HttpGraphQlClient client = ... ;
DgsGraphQlClient dgsClient = DgsGraphQlClient.create(client); // <1>
List<Book> books = dgsClient.request(new BooksGraphQLQuery()) // <2>
.projection(new BooksProjectionRoot<>().id().name()) // <3>
.retrieveSync("books")
.toEntityList(Book.class);
----
<1> - Create `DgsGraphQlClient` by wrapping any `GraphQlClient`.
<2> - Specify the operation for the request.
<3> - Define the selection set.