Client

Spring for GraphQL includes client support to execute GraphQL requests over HTTP, WebSocket, and RSocket.

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:

Each defines a Builder with options relevant to the transport. All builders extend from a common, base GraphQlClient Builder with options applicable to all transports.

Once GraphQlClient is built you can begin to make requests.

Typically, the GraphQL operation for a request is provided as text. Alternatively, you can use DGS Codegen client API classes through DgsGraphQlClient, which can wrap any of the above GraphQlClient extensions.

HTTP Sync

HttpSyncGraphQlClient uses RestClient to execute GraphQL requests over HTTP through a blocking transport contract and chain of interceptors.

RestClient restClient = RestClient.create("https://spring.io/graphql");
HttpSyncGraphQlClient graphQlClient = HttpSyncGraphQlClient.create(restClient);

Once HttpSyncGraphQlClient is created, you can begin to 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:

RestClient restClient = RestClient.create("https://spring.io/graphql");
HttpSyncGraphQlClient graphQlClient = HttpSyncGraphQlClient.builder(restClient)
		.headers((headers) -> headers.setBasicAuth("joe", "..."))
		.build();

// Perform requests with graphQlClient...

HttpSyncGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
		.headers((headers) -> headers.setBasicAuth("peter", "..."))
		.build();

// Perform requests with anotherGraphQlClient...

HTTP

HttpGraphQlClient uses WebClient to execute GraphQL requests over HTTP through a non-blocking transport contract and chain of interceptors.

WebClient webClient = WebClient.create("https://spring.io/graphql");
HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);

Once HttpGraphQlClient is created, you can begin to 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:

WebClient webClient = WebClient.create("https://spring.io/graphql");

HttpGraphQlClient graphQlClient = HttpGraphQlClient.builder(webClient)
		.headers((headers) -> headers.setBasicAuth("joe", "..."))
		.build();

// Perform requests with graphQlClient...

HttpGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
		.headers((headers) -> headers.setBasicAuth("peter", "..."))
		.build();

// Perform requests with anotherGraphQlClient...

WebSocket

WebSocketGraphQlClient executes GraphQL requests over a shared WebSocket connection. It is built using the WebSocketClient from Spring WebFlux and you can create it as follows:

String url = "wss://spring.io/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client).build();

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.

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 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:

String url = "wss://spring.io/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
		.headers((headers) -> headers.setBasicAuth("joe", "..."))
		.build();

// Use graphQlClient...

WebSocketGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
		.headers((headers) -> headers.setBasicAuth("peter", "..."))
		.build();

// Use anotherGraphQlClient...

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:

String url = "wss://spring.io/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
		.keepAlive(Duration.ofSeconds(30))
		.build();

Interceptor

The 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:

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
	}

}

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.

RSocket

RSocketGraphQlClient uses RSocketRequester to execute GraphQL requests over RSocket requests.

URI uri = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(uri);

RSocketGraphQlClient client = RSocketGraphQlClient.builder()
		.clientTransport(transport)
		.build();

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.

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 execute requests using the same API, independent of the underlying transport.

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

  • 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.

Requests

Once you have a GraphQlClient, you can begin to perform requests via retrieve or execute methods.

Retrieve

The below retrieves and decodes the data for a query:

  • Sync

  • Non-Blocking

String document =
	"""
	{
		project(slug:"spring-framework") {
			name
			releases {
				version
			}
		}
	}
	""";

Project project = graphQlClient.document(document) (1)
	.retrieveSync("project") (2)
	.toEntity(Project.class); (3)
String document =
	"""
	{
		project(slug:"spring-framework") {
			name
			releases {
				version
			}
		}
	}
	""";

Mono<Project> project = graphQlClient.document(document) (1)
		.retrieve("project") (2)
		.toEntity(Project.class); (3)
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 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:

  • Sync

  • Non-Blocking

try {
	Project project = graphQlClient.document(document)
			.retrieveSync("project")
			.toEntity(Project.class);
	return project;
}
catch (FieldAccessException ex) {
	ClientGraphQlResponse response = ex.getResponse();
	// ...
	ClientResponseField field = ex.getField();
	// return fallback value
	return new Project();
}
Mono<Project> projectMono = graphQlClient.document(document)
		.retrieve("project")
		.toEntity(Project.class)
		.onErrorResume(FieldAccessException.class, (ex) -> {
			ClientGraphQlResponse response = ex.getResponse();
			// ...
			ClientResponseField field = ex.getField();
			// return fallback value
			return Mono.just(new Project());
		});

Execute

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:

  • Sync

  • Non-Blocking

ClientGraphQlResponse response = graphQlClient.document(document).executeSync();

if (!response.isValid()) {
	// Request failure... (1)
}

ClientResponseField field = response.field("project");
if (field.getValue() == null) {
	if (field.getErrors().isEmpty()) {
		// Optional field set to null... (2)
	}
	else {
		// Field failure... (3)
	}
}

Project project = field.toEntity(Project.class); (4)
Mono<Project> projectMono = graphQlClient.document(document)
		.execute()
		.map((response) -> {
			if (!response.isValid()) {
				// Request failure... (1)
			}

			ClientResponseField field = response.field("project");
			if (field.getValue() == null) {
				if (field.getErrors().isEmpty()) {
					// Optional field set to null... (2)
				}
				else {
					// Field failure... (3)
				}
			}

			return field.toEntity(Project.class); (4)
		});
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

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:

src/main/resources/graphql-documents/projectReleases.graphql
query projectReleases($slug: ID!) {
	project(slug: $slug) {
		name
		releases {
			version
		}
	}
}

You can then:

Project project = graphQlClient.documentName("projectReleases") (1)
		.variable("slug", "spring-framework") (2)
		.retrieveSync("projectReleases.project")
		.toEntity(Project.class);
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 Builder to customize the DocumentSource for loading documents by names.

Subscription Requests

Subscription requests require a client transport that is capable of streaming data. You will need to create a GraphQlClient that support this:

Retrieve

To start a subscription stream, use retrieveSubscription which is similar to retrieve for a single response but returning a stream of responses, each decoded to some data:

Flux<String> greetingFlux = client.document("subscription { greetings }")
		.retrieveSubscription("greeting")
		.toEntity(String.class);

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.

Execute

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:

Flux<String> greetingFlux = client.document("subscription { greetings }")
		.executeSubscription()
		.map((response) -> {
			if (!response.isValid()) {
				// Request failure...
			}

			ClientResponseField field = response.field("project");
			if (field.getValue() == null) {
				if (field.getErrors().isEmpty()) {
					// Optional field set to null...
				}
				else {
					// Field failure...
				}
			}

			return field.toEntity(String.class);
		});

Interception

For blocking transports created with the GraphQlClient.SyncBuilder, you create a SyncGraphQlClientInterceptor to intercept all requests through the client:

import org.springframework.graphql.client.ClientGraphQlRequest;
import org.springframework.graphql.client.ClientGraphQlResponse;
import org.springframework.graphql.client.SyncGraphQlClientInterceptor;

public class SyncInterceptor implements SyncGraphQlClientInterceptor {

	@Override
	public ClientGraphQlResponse intercept(ClientGraphQlRequest request, Chain chain) {
		// ...
		return chain.next(request);
	}
}

For non-blocking transports created with GraphQlClient.Builder, you create a GraphQlClientInterceptor to intercept all requests through the client:

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.springframework.graphql.client.ClientGraphQlRequest;
import org.springframework.graphql.client.ClientGraphQlResponse;
import org.springframework.graphql.client.GraphQlClientInterceptor;

public class MyInterceptor implements GraphQlClientInterceptor {

	@Override
	public Mono<ClientGraphQlResponse> intercept(ClientGraphQlRequest request, Chain chain) {
		// ...
		return chain.next(request);
	}

	@Override
	public Flux<ClientGraphQlResponse> interceptSubscription(ClientGraphQlRequest request, SubscriptionChain chain) {
		// ...
		return chain.next(request);
	}

}

Once the interceptor is created, register it through the client builder. For example:

URI url = URI.create("wss://localhost:8080/graphql");
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
		.interceptor(new MyInterceptor())
		.build();

DGS Codegen

As an alternative to providing the operation such as a mutation, query, or subscription as text, you can use the DGS Codegen library to generate client API classes that let you use a fluent API to define the request.

Spring for GraphQL provides DgsGraphQlClient that wraps any GraphQlClient and helps to prepare the request with generated client API classes.

For example, given the following schema:

type Query {
    books: [Book]
}

type Book {
    id: ID
    name: String
}

You can perform a request as follows:

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.