Implementing GraphQL APIs in Spring Boot: Multiple Approaches
Spring Boot integrates with GraphQL to provide flexible API development. Several methods exist for constructing a GraphQL schema and resolving data.
Approach One: Utilizing graphql-java-tools
Include the necessary dependencies.
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>5.6.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphiql-spring-boot-starter</artifactId>
<version>5.0.4</version>
</dependency>
Define a GraphQL Schema File (schema.graphqls).
type Query {
productList: [Product!]
}
type Product {
sku: Int!
title: String!
manufacturer: Manufacturer!
}
type Manufacturer {
code: Int!
companyName: String!
}
Create corresponding Java domain classes.
public class Product {
private int sku;
private String title;
private int manufacturerCode;
// Constructors, getters, and setters
}
public class Manufacturer {
private int code;
private String companyName;
// Constructors, getters, and setters
}
Implement a resolver for complex fields on the Product type.
@Component
public class ProductResolver implements GraphQLResolver<Product> {
private ManufacturerRepository manufacturerRepo;
public ProductResolver(ManufacturerRepository manufacturerRepo) {
this.manufacturerRepo = manufacturerRepo;
}
public Manufacturer manufacturer(Product product) {
return manufacturerRepo.findByCode(product.getManufacturerCode());
}
}
Implement the root query resolver.
@Component
public class QueryResolver implements GraphQLQueryResolver {
private ProductRepository productRepo;
public QueryResolver(ProductRepository productRepo) {
this.productRepo = productRepo;
}
public List<Product> productList() {
return productRepo.getAllProducts();
}
}
If a GraphQL type's fields directly correspond to a Java class's properties, a specific resolver for that type is optional.
Field Resolution Priority When mapping a GraphQL object type field to a Java class, the library searches in this order:
- Method
fieldName(*fieldArgs [, DataFetchingEnvironment]) - Method
isFieldName(*fieldArgs [, DataFetchingEnvironment])(for Boolean fields) - Method
getFieldName(*fieldArgs [, DataFetchingEnvironment]) - Method
getFieldFieldName(*fieldArgs [, DataFetchingEnvironment]) - Field
fieldName
Resolver method are checked before the Java class methods. For the manufacturer field in the Product type, the ProductResolver.manufacturer(Product) method takes precedence.
Build the executable schema.
@Configuration
public class GraphQLConfig {
@Bean
public GraphQLSchema schema(QueryResolver queryResolver, ProductResolver productResolver) {
return SchemaParser.newParser()
.file("schema.graphqls")
.resolvers(queryResolver, productResolver)
.build()
.makeExecutableSchema();
}
}
Approach Two: Manual Schema Wiring with DataFetchers
This method does not require graphql-java-tools. Define a schema file.
type Query {
findProduct(sku: ID): Product
}
type Product {
sku: ID
title: String
pageCount: Int
manufacturer: Manufacturer
}
type Manufacturer {
code: ID
firstName: String
lastName: String
}
Load the schema and configure runtime wiring with DataFetcher implementations.
@Configuration
public class GraphQLManualConfig {
@Value("classpath:schema.graphqls")
private Resource schemaResource;
@Bean
public GraphQL graphQL(GraphQLDataProvider dataProvider) throws IOException {
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(schemaResource.getFile());
RuntimeWiring wiring = buildRuntimeWiring(dataProvider);
SchemaGenerator schemaGenerator = new SchemaGenerator();
GraphQLSchema schema = schemaGenerator.makeExecutableSchema(typeRegistry, wiring);
return GraphQL.newGraphQL(schema).build();
}
private RuntimeWiring buildRuntimeWiring(GraphQLDataProvider dataProvider) {
return RuntimeWiring.newRuntimeWiring()
.type(TypeRuntimeWiring.newTypeWiring("Query")
.dataFetcher("findProduct", dataProvider.getProductDataFetcher()))
.type(TypeRuntimeWiring.newTypeWiring("Product")
.dataFetcher("manufacturer", dataProvider.getManufacturerDataFetcher()))
.build();
}
}
Implement the DataFetcher component. The fetchers return Map objects matching the expected structure.
@Component
public class GraphQLDataProvider {
private final List<Map<String, String>> productCatalog = Arrays.asList(
Map.of("sku", "P001", "title", "Effective Java", "pageCount", "416", "manufacturerCode", "M001"),
Map.of("sku", "P002", "title", "Clean Code", "pageCount", "464", "manufacturerCode", "M002")
);
private final List<Map<String, String>> manufacturers = Arrays.asList(
Map.of("code", "M001", "firstName", "Joshua", "lastName", "Bloch"),
Map.of("code", "M002", "firstName", "Robert", "lastName", "Martin")
);
public DataFetcher getProductDataFetcher() {
return environment -> {
String sku = environment.getArgument("sku");
return productCatalog.stream()
.filter(product -> sku.equals(product.get("sku")))
.findFirst()
.orElse(null);
};
}
public DataFetcher getManufacturerDataFetcher() {
return environment -> {
Map<String, String> product = environment.getSource();
String manCode = product.get("manufacturerCode");
return manufacturers.stream()
.filter(manufacturer -> manCode.equals(manufacturer.get("code")))
.findFirst()
.orElse(null);
};
}
}
This approach does not mandate specific Java classes. DataFetchers must return data structures compatible with the GraphQL type definitions.
Approach Three: Programmatic Schema Definition
Construct the GraphQL schema entirely in code without a .graphqls file.
@Configuration
public class ProgrammaticGraphQLConfig {
@Bean
public GraphQL graphQL() {
// Define a GraphQL Object Type
GraphQLObjectType itemType = GraphQLObjectType.newObject()
.name("InventoryItem")
.field(GraphQLFieldDefinition.newFieldDefinition()
.name("description")
.type(Scalars.GraphQLString))
.build();
// Define a DataFetcher for the type's field
DataFetcher<String> descriptionFetcher = environment -> {
// Business logic to fetch data
return "Sample Item Description";
};
// Define the root Query type
GraphQLObjectType queryType = GraphQLObjectType.newObject()
.name("RootQuery")
.field(GraphQLFieldDefinition.newFieldDefinition()
.name("item")
.type(itemType)
.dataFetcher(descriptionFetcher))
.build();
// Create the executable schema
GraphQLSchema schema = GraphQLSchema.newSchema()
.query(queryType)
.build();
return GraphQL.newGraphQL(schema).build();
}
}
The programmatic definition above is equivalent to the following SDL:
type InventoryItem {
description: String
}
type RootQuery {
item: InventoryItem
}
A simple execution example:
GraphQL graphQL = // obtain GraphQL instance
ExecutionResult result = graphQL.execute("{ item { description } }");
String description = result.getData("item.description"); // "Sample Item Description"