Type-safe GraphQL client library for Spring Boot with code generation.
- Type-safe query builder - No
.graphqlfiles needed, build queries programmatically - Code generation - Generate Java types from
schema.json - Spring Boot integration - Auto-configuration with metrics, tracing, and health checks
- Gradle & Maven plugins - Seamless build tool integration
- Zero runtime reflection - Fast and GraalVM-friendly
Gradle (Kotlin DSL)
plugins {
id("io.github.graphite") version "0.1.0"
}
dependencies {
implementation("io.github.graphite:graphite-spring-boot-starter:0.1.0")
testImplementation("io.github.graphite:graphite-test-utils:0.1.0")
}
graphite {
schemaFile = file("src/main/resources/graphql/schema.json")
packageName = "com.example.graphql"
}Maven
<plugin>
<groupId>io.github.graphite</groupId>
<artifactId>graphite-maven-plugin</artifactId>
<version>0.1.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<schemaFile>${project.basedir}/src/main/resources/graphql/schema.json</schemaFile>
<packageName>com.example.graphql</packageName>
</configuration>
</plugin>
<dependencies>
<dependency>
<groupId>io.github.graphite</groupId>
<artifactId>graphite-spring-boot-starter</artifactId>
<version>0.1.0</version>
</dependency>
<dependency>
<groupId>io.github.graphite</groupId>
<artifactId>graphite-test-utils</artifactId>
<version>0.1.0</version>
<scope>test</scope>
</dependency>
</dependencies>Add to application.yml:
graphite:
url: https://api.example.com/graphql
timeout:
connect: 10s
read: 30s
request: 60s
retry:
max-attempts: 3
initial-delay: 100ms
headers:
Authorization: Bearer ${GRAPHQL_TOKEN}Place your GraphQL introspection schema at src/main/resources/graphql/schema.json. Generate it from your GraphQL server:
# Using graphql-cli
graphql get-schema --endpoint https://api.example.com/graphql --json > src/main/resources/graphql/schema.json# Gradle
./gradlew graphiteGenerate
# Maven
mvn graphite:generateThis generates type-safe Java classes in build/generated/sources/graphite/ (Gradle) or target/generated-sources/graphite/ (Maven).
import org.springframework.stereotype.Service;
import io.github.graphite.GraphiteClient;
import com.example.graphql.query.GetUserQuery;
import com.example.graphql.type.UserDTO;
@Service
public class UserService {
private final GraphiteClient client;
public UserService(GraphiteClient client) {
this.client = client;
}
public UserDTO getUser(String id) {
var response = client.execute(
GetUserQuery.builder()
.id(id)
.selecting(s -> s.id().name().email().createdAt())
.build()
);
return response.getDataOrThrow();
}
}import io.github.graphite.test.GraphiteMockServer;
import io.github.graphite.test.GraphiteAssertions;
import org.junit.jupiter.api.Test;
import java.util.Map;
class UserServiceTest {
@Test
void shouldFetchUser() {
try (GraphiteMockServer server = GraphiteMockServer.create()) {
// Stub the GraphQL response
server.stubQuery("GetUser", Map.of(
"id", "123",
"name", "John Doe",
"email", "john@example.com"
));
// Configure client to use mock server
GraphiteClient client = GraphiteClient.builder()
.endpoint(URI.create(server.getUrl()))
.build();
// Execute and verify
UserDTO user = new UserService(client).getUser("123");
assertThat(user.name()).isEqualTo("John Doe");
server.verify("GetUser", 1);
}
}
}Configure custom scalar type mappings in your build tool:
Gradle
graphite {
schemaFile = file("src/main/resources/graphql/schema.json")
packageName = "com.example.graphql"
scalars = mapOf(
"DateTime" to "java.time.OffsetDateTime",
"Date" to "java.time.LocalDate",
"Time" to "java.time.LocalTime",
"JSON" to "com.fasterxml.jackson.databind.JsonNode",
"Long" to "java.lang.Long",
"UUID" to "java.util.UUID"
)
}Maven
<configuration>
<schemaFile>${project.basedir}/src/main/resources/graphql/schema.json</schemaFile>
<packageName>com.example.graphql</packageName>
<scalars>
<DateTime>java.time.OffsetDateTime</DateTime>
<Date>java.time.LocalDate</Date>
<JSON>com.fasterxml.jackson.databind.JsonNode</JSON>
</scalars>
</configuration>Graphite provides a rich exception hierarchy for precise error handling:
import io.github.graphite.exception.*;
try {
var response = client.execute(query);
return response.getDataOrThrow();
} catch (GraphiteConnectionException e) {
// Network connectivity issues
log.error("Connection failed: {}", e.getMessage());
throw new ServiceUnavailableException("GraphQL server unreachable");
} catch (GraphiteTimeoutException e) {
// Request timed out
if (e.isSafeToRetry()) {
// Connect timeout - no request was sent, safe to retry
return retryRequest(query);
}
throw new TimeoutException("Request timed out: " + e.getTimeoutType());
} catch (GraphiteRateLimitException e) {
// Rate limit exceeded
log.warn("Rate limited: {}", e.getMessage());
throw new TooManyRequestsException();
} catch (GraphiteServerException e) {
// Server returned 5xx error
log.error("Server error (HTTP {}): {}", e.getStatusCode(), e.getMessage());
throw new InternalServerException();
} catch (GraphiteGraphQLException e) {
// GraphQL-level errors in response
log.warn("GraphQL errors: {}", e.getErrors());
throw new BadRequestException(e.getMessage());
} catch (GraphiteException e) {
// Catch-all for any Graphite error
log.error("Unexpected error: {}", e.getMessage(), e);
throw new InternalServerException();
}For partial responses (data with errors), check the response directly:
var response = client.execute(query);
if (response.hasErrors()) {
response.getErrors().forEach(error ->
log.warn("GraphQL warning: {} at {}", error.message(), error.path())
);
}
if (response.hasData()) {
return response.getData();
}
throw new NoDataException("No data in response");Configure retry behavior programmatically:
import io.github.graphite.retry.*;
// Exponential backoff with custom settings
var backoff = ExponentialBackoff.builder()
.initialDelay(Duration.ofMillis(100))
.maxDelay(Duration.ofSeconds(10))
.multiplier(2.0)
.build();
// Add jitter to prevent thundering herd
var jitteredBackoff = backoff.withJitter(0.25);
// Create retry policy
var retryPolicy = RetryPolicy.builder()
.maxAttempts(5)
.backoffStrategy(jitteredBackoff)
.retryOn(GraphiteConnectionException.class)
.retryOn(GraphiteTimeoutException.class)
.retryOn(GraphiteServerException.class)
.build();
// Use with client
var client = GraphiteClient.builder()
.endpoint("https://api.example.com/graphql")
.retryPolicy(retryPolicy)
.build();Or via Spring Boot configuration:
graphite:
url: https://api.example.com/graphql
retry:
enabled: true
max-attempts: 5
initial-delay: 100ms
multiplier: 2.0
max-delay: 10sGraphite automatically exposes Micrometer metrics when the starter is used:
| Metric | Type | Tags | Description |
|---|---|---|---|
graphite.client.requests |
Counter | operation, status | Total requests |
graphite.client.request.duration |
Timer | operation | Request duration |
graphite.client.errors |
Counter | operation, error_type | Error count |
graphite.client.retry.attempts |
Counter | exception_type | Retry attempts |
graphite.client.retry.exhausted |
Counter | exception_type | Exhausted retries |
graphite.client.retry.success |
Counter | - | Successful retries |
Access metrics programmatically:
import io.github.graphite.spring.observability.GraphiteMetrics;
@Service
public class GraphQLMonitoringService {
private final GraphiteMetrics metrics;
public GraphQLMonitoringService(GraphiteMetrics metrics) {
this.metrics = metrics;
}
public void executeWithCustomMetrics(GraphQLOperation<?> operation) {
var sample = metrics.startTimer();
try {
client.execute(operation);
metrics.recordSuccess(operation.operationName(), sample);
} catch (Exception e) {
metrics.recordError(operation.operationName(), e, sample);
throw e;
}
}
}Enable distributed tracing with Micrometer Tracing:
# application.yml
management:
tracing:
enabled: true
sampling:
probability: 1.0 # Sample all requests (adjust for production)Graphite propagates trace context automatically in requests.
Enable the health indicator:
management:
health:
graphite:
enabled: true
endpoint:
health:
show-details: alwaysThe health check performs a lightweight introspection query to verify endpoint availability.
| Module | Description |
|---|---|
graphite-core |
Runtime client library |
graphite-codegen |
Code generation engine |
graphite-gradle-plugin |
Gradle plugin for code generation |
graphite-maven-plugin |
Maven plugin for code generation |
graphite-spring-boot-starter |
Spring Boot auto-configuration |
graphite-test-utils |
Testing utilities |
- Java 21+
- Gradle 8.5+ or Maven 3.8+
- Spring Boot 3.x (for starter module)
graphite:
# Required: GraphQL endpoint URL
url: https://api.example.com/graphql
# Optional: Static headers
headers:
Authorization: Bearer ${TOKEN}
X-Custom-Header: value
# Optional: Timeout configuration
timeout:
connect: 10s # Connection timeout (default: 10s)
read: 30s # Read timeout (default: 30s)
request: 60s # Overall request timeout (default: 60s)
# Optional: Retry configuration
retry:
max-attempts: 3 # Max retry attempts (default: 3)
initial-delay: 100ms # Initial delay between retries (default: 100ms)
multiplier: 2.0 # Backoff multiplier (default: 2.0)
max-delay: 5s # Maximum delay between retries (default: 5s)
# Optional: Rate limiting
rate-limit:
requests-per-second: 100 # Max requests per second (default: unlimited)
burst-capacity: 150 # Burst capacity (default: same as rps)
# Optional: Connection pool
connection-pool:
max-connections: 50 # Maximum connections (default: 50)
idle-timeout: 30s # Idle connection timeout (default: 30s)Graphite integrates with Spring Boot Actuator for observability:
- Metrics - Request duration, success/error counts via Micrometer
- Tracing - Distributed tracing with trace context propagation
- Health - Health indicator for GraphQL endpoint availability
Enable in application.yml:
management:
endpoints:
web:
exposure:
include: health,metrics
health:
graphite:
enabled: trueThe graphite-test-utils module provides:
- GraphiteMockServer - WireMock-based mock GraphQL server
- GraphiteRequestMatcher - Fluent request matching
- GraphiteResponseBuilder - Fluent response building
- GraphiteAssertions - Fluent assertions for responses
Example with advanced matching:
try (GraphiteMockServer server = GraphiteMockServer.create()) {
// Match requests with specific variables and headers
GraphiteRequestMatcher matcher = GraphiteRequestMatcher.forQuery("GetUser")
.withVariable("id", "123")
.withBearerToken("secret-token");
server.stub(matcher, Map.of("id", "123", "name", "John"));
// ... execute request ...
server.verify("GetUser", 1);
}git clone --recurse-submodules https://github.com/philipp-gatzka/graphite.git
cd graphite
./scripts/setup.sh
./gradlew buildThis project is licensed under the Apache License 2.0 - see the LICENSE file for details.