GraphQL in Spring Boot
Schema-first wiring, controllers and testing.
Open this lesson in the learning hubKey points
- Spring for GraphQL is schema-first: the
.graphqlsfile undersrc/main/resources/graphql/is the contract, and Java code implements it. - That direction matters. The schema is what clients depend on, so generating it from Java would make every refactor a potentially breaking API change.
@QueryMappingand@MutationMappingbind root fields;@SchemaMappingbinds a field on a type, which is where nested resolvers live.@Argumentbinds an argument, and Spring maps input objects onto records automatically - the natural shape for an input type.- GraphiQL is available at
/graphiqlwhen enabled. It is a development tool and must be disabled in production, along with introspection. GraphQlTestermakes testing pleasant: send a document, then assert on paths in the response rather than parsing JSON by hand.
Example
# src/main/resources/graphql/schema.graphqls
type Query {
order(id: ID!): Order
orders(first: Int = 20, after: String): OrderConnection!
}
type Mutation {
placeOrder(input: PlaceOrderInput!): PlaceOrderPayload!
}
input PlaceOrderInput { customerId: ID!, items: [OrderItemInput!]! }
type PlaceOrderPayload { order: Order, errors: [UserError!]! }
type UserError { field: String, message: String! }
---
@Controller
class OrderGraphQlController {
@QueryMapping
public Order order(@Argument String id) {
return service.find(Long.valueOf(id));
}
// A field ON Order - this is the one that needs a DataLoader.
@SchemaMapping(typeName = "Order", field = "customer")
public CompletableFuture<Customer> customer(Order order,
DataLoader<Long, Customer> loader) {
return loader.load(order.customerId());
}
// Input objects bind straight onto a record.
@MutationMapping
public PlaceOrderPayload placeOrder(@Argument PlaceOrderInput input) {
// Expected, user-facing failures belong in the PAYLOAD, not as
// a thrown error - clients can then handle them per field.
var violations = validator.validate(input);
if (!violations.isEmpty()) {
return new PlaceOrderPayload(null, toUserErrors(violations));
}
return new PlaceOrderPayload(service.place(input), List.of());
}
}
---
# Production settings - GraphiQL and introspection OFF.
spring:
graphql:
graphiql:
enabled: false # true only in dev
schema:
introspection:
enabled: false # do not publish the schema publicly
---
@GraphQlTest(OrderGraphQlController.class)
class OrderGraphQlControllerTest {
@Autowired GraphQlTester tester;
@MockBean OrderService service;
@Test
void fetchesAnOrder() {
when(service.find(1L)).thenReturn(new Order(1L, "PAID"));
tester.document("{ order(id: \"1\") { id status } }")
.execute()
.path("order.status").entity(String.class).isEqualTo("PAID");
}
}
Schema-first keeps the contract stable, @SchemaMapping is where nested resolvers and DataLoaders go, and introspection is off in production.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the GraphQL Course course, and every lesson in it is listed on the GraphQL Course contents page.