[MM’s] Boot Notes — Building Smarter APIs with Spring Boot and GraphQL
How GraphQL simplifies data fetching and API design for modern Spring apps

Every backend developer knows the “data dilemma”: REST gives you too much or too little, SOAP gives you XML headaches and gRPC gives you performance with rigidity.
GraphQL changes the game — letting clients ask exactly for what they need, nothing more, nothing less.
With Spring Boot’s native GraphQL starter, you can now build these smarter APIs with zero boilerplate.
⚡ TL;DR (Quick Recap)
- Spring Boot with native GraphQL support via spring-boot-starter-graphql.
- Schema-first development: Define types and operations, then auto-generate Java classes.
- GraphiQL UI: Explore and test your API live.
- Annotation-based mappings: Use @QueryMapping and @MutationMapping
- Built-in error handling and type safety make GraphQL a natural fit for Spring developers.
GraphQL’s Rise in the Spring Ecosystem
The Spring team has aligned GraphQL’s flexibility with its dependency injection and controller paradigms. With Spring Boot, developers get:
- A single /graphql endpoint for all operations.
- Integrated GraphiQL web UI for live queries.
- Full schema inspection and observability support.
- Out-of-the-box code generation for schema-defined types.
GraphQL fits perfectly into modern architectures where clients — especially front-end and mobile apps — demand selective data retrieval and predictable APIs.
When to Choose GraphQL
Best suited for:
- Mobile apps with limited bandwidth (fetch only needed fields)
- Complex UIs with nested data requirements
- Multiple clients with different data needs
- Rapid frontend iteration without backend changes
Consider REST/gRPC when:
- Simple CRUD operations with fixed data structures
- High-throughput microservice-to-microservice communication
- File uploads or binary data transfer
- Strict RESTful caching requirements
Schema-First, the Spring Way
At the heart of every GraphQL API lies the schema. In Spring Boot, you simply drop a .graphqls file into src/main/resources/graphql/:
type Query {
product(id: ID!): Product!
products(page: Int = 0, size: Int = 10): ProductPage!
}
type Mutation {
createProduct(input: CreateProductInput!): CreateProductPayload!
updateProduct(id: ID!, input: UpdateProductInput!): UpdateProductPayload!
deleteProduct(id: ID!): DeleteProductPayload!
}
type Product {
id: ID!
name: String!
data: ProductData
}
type ProductData {
color: String
capacity: String
generation: String
}
...Spring Boot detects and wires these definitions into the GraphQL engine.
Generating Java Code from the Schema
Instead of manually creating DTOs, you can integrate a code generation plugin like the popular graphqlcodegen-maven-plugin to create them automatically from the schema.
<plugin>
<groupId>io.github.deweyjose</groupId>
<artifactId>graphqlcodegen-maven-plugin</artifactId>
<version>${graphql-codegen-maven-plugin.version}</version>
<executions>
<execution>
<goals><goal>generate</goal></goals>
<configuration>
<schemaPaths><param>src/main/resources/graphql</param></schemaPaths>
<packageName>io.github.mm.graphql.product.graphql.model</packageName>
</configuration>
</execution>
</executions>
</plugin>
This ensures type safety and consistent contract alignment between schema and Java code.
Defining Your API Logic
Spring makes it straightforward to bind GraphQL fields to methods using annotations:
@Controller
public class ProductApi {
// ....
@QueryMapping
public Product product(@Argument String id) {
return service.getProductById(id);
}
@MutationMapping
public CreateProductPayload createProduct(@Argument CreateProductInput input) {
return service.createProduct(input);
}
}
Here:
- @QueryMapping corresponds to GraphQL’s Query operations.
- @MutationMapping handles data modification.
- @Argument extracts query parameters from the GraphQL request.
This declarative style mirrors the familiar Spring MVC model while providing GraphQL’s precision.
Built-In GraphiQL UI Explorer
Enable GraphiQL in application.properties:
spring.graphql.graphiql.enabled=true
spring.graphql.graphiql.path=/graphiql
Now visit http://localhost:8080/graphiql to write and execute queries interactively. You can explore your schema, run mutations and see live JSON responses — all without external tools.
Example query:
query {
products(page: 0, size: 10) {
content {
id
name
data {
color
capacity
generation
}
}
totalElements
totalPages
pageNumber
pageSize
}
}Error Handling That Feels Like Spring
Spring integrates seamlessly with GraphQL’s error model via a DataFetcherExceptionResolver:
@Component
public class GraphQlExceptionHandler extends DataFetcherExceptionResolverAdapter {
@Override
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
if (ex instanceof NotFoundException nf) {
return GraphqlErrorBuilder.newError()
.errorType(ErrorType.NOT_FOUND)
.message(nf.getMessage())
.path(env.getExecutionStepInfo().getPath())
.build();
}
return null;
}
}
The result? Clean, structured error responses such as:
{
"errors": [
{
"message": "Product not found with id: 123",
"path": ["product"],
"extensions": { "errorType": "NOT_FOUND" }
}
]
}Or more modern alternative:
@ControllerAdvice
public class GraphQlExceptionHandler {
@GraphQlExceptionHandler
public GraphQLError handleNotFoundException(NotFoundException ex, DataFetchingEnvironment env) {
return GraphqlErrorBuilder.newError()
.errorType(ErrorType.NOT_FOUND)
.message(ex.getMessage())
.path(env.getExecutionStepInfo().getPath())
.build();
}
// ... other exception handlers
}
Common Pitfalls to Avoid
1. N+1 Query Problem ✗ Fetching related data without batching — Use DataLoader pattern for batch fetching
2. Over-exposing Data ✗ Returning all fields from database entities — Use dedicated GraphQL types that expose only necessary fields
3. Missing Error Handling ✗ Generic exceptions that leak implementation details — Structured errors with proper error types
4. No Query Complexity Limits ✗ Allowing arbitrarily deep/expensive queries — Implement depth and complexity instrumentation
5. Forgetting Nullability ✗ Making everything non-null with ! in schema — Use nullable by default, mark required fields explicitly
Testing Your GraphQL Endpoints
Spring’s GraphQlTester simplifies automated testing:
@SpringBootTest
@AutoConfigureGraphQlTester
class ProductApiTests {
@Autowired
GraphQlTester tester;
@Test
void shouldCreateAndFetchProduct() {
tester.document("""
mutation {
createProduct(input: { name: "MacBook Air", data: { color: "Silver" } }) {
product { id name }
}
}
""").execute().path("createProduct.product.name").entity(String.class).isEqualTo("MacBook Air");
}
}
This lets you verify your GraphQL operations end-to-end without spinning up a frontend or using Postman — perfect for CI pipelines.
Comparing GraphQL with REST, SOAP and gRPC
Spring now supports multiple paradigms for APIs. Here’s how they differ conceptually:
SOAP
- XML-based, verbose, and heavy on contracts.
- Great for enterprise legacy systems, but not flexible for modern front ends.
REST
- Simpler than SOAP; each resource exposed via endpoints.
- Suffers from over-fetching (too much data) or under-fetching (multiple calls).
gRPC
- Binary protocol ideal for microservices communication.
- Efficient, but less human-friendly.
GraphQL
- One endpoint, declarative queries, and introspection built-in.
- Clients choose exactly what data to fetch — ideal for UI-driven applications.
In the Spring ecosystem, GraphQL doesn’t replace REST or gRPC — it complements them. Use GraphQL when clients demand flexibility and rich object graphs; use REST or gRPC for fixed contracts or high-performance internal APIs.
Final Takeaways
GraphQL isn’t just another API buzzword — it’s a shift toward client-driven data exchange. With Spring Boot native integration, developers can build query-driven APIs that feel as expressive as SQL, but run with Spring Boot simplicity.
Whether you’re designing APIs for mobile clients or modern dashboards, GraphQL brings precision, type safety and developer joy to the Spring ecosystem.
You can find all the code on GitHub.
Originally posted on marconak-matej.medium.com.