Why GraphQL?
REST APIs have a problem: you get what the server decides, not what you need. Want just the user's name? Too bad, here's their entire profile. Need data from 3 endpoints? Make 3 requests.
GraphQL flips this: clients request exactly what they need in a single query. No over-fetching, no under-fetching.
// REST: Multiple requests, over-fetching
GET /users/1 → { id, name, email, address, phone, ... }
GET /users/1/posts → [ { id, title, content, ... }, ... ]
GET /users/1/friends → [ ... ]
// GraphQL: One request, exact data
query {
user(id: 1) {
name
posts {
title
}
friends {
name
}
}
}
Setup with Spring Boot
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
# application.properties spring.graphql.graphiql.enabled=true spring.graphql.path=/graphql
Define Your Schema
Create src/main/resources/graphql/schema.graphqls:
type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User
deleteUser(id: ID!): Boolean!
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
input CreateUserInput {
name: String!
email: String!
}
input UpdateUserInput {
name: String
email: String
}
Implement Resolvers
@Controller
public class UserController {
@Autowired
private UserService userService;
// Query resolver
@QueryMapping
public List<User> users() {
return userService.findAll();
}
@QueryMapping
public User user(@Argument Long id) {
return userService.findById(id);
}
// Mutation resolver
@MutationMapping
public User createUser(@Argument CreateUserInput input) {
return userService.create(input.getName(), input.getEmail());
}
@MutationMapping
public User updateUser(@Argument Long id, @Argument UpdateUserInput input) {
return userService.update(id, input);
}
@MutationMapping
public boolean deleteUser(@Argument Long id) {
return userService.delete(id);
}
// Field resolver - loads posts for a user
@SchemaMapping(typeName = "User", field = "posts")
public List<Post> posts(User user) {
return postService.findByAuthorId(user.getId());
}
}
Making Queries
# Get all users with their posts
query {
users {
id
name
posts {
title
}
}
}
# Get specific user
query {
user(id: "1") {
name
email
}
}
# Create a user
mutation {
createUser(input: { name: "John", email: "john@email.com" }) {
id
name
}
}
# Variables (better for dynamic data)
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
}
}
# With variables:
# { "input": { "name": "John", "email": "john@email.com" } }
N+1 Problem & DataLoader
Without optimization, fetching users with posts makes N+1 database calls. DataLoader batches these.
@Configuration
public class DataLoaderConfig {
@Bean
public BatchLoaderRegistry batchLoaderRegistry(PostService postService) {
return registry -> {
registry.forTypePair(Long.class, List.class)
.registerMappedBatchLoader((userIds, env) -> {
// Single query for all posts
Map<Long, List<Post>> postsByUser = postService.findByAuthorIds(userIds);
return Mono.just(postsByUser);
});
};
}
}
Error Handling
@ControllerAdvice
public class GraphQLExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public GraphQLError handleUserNotFound(UserNotFoundException ex) {
return GraphQLError.newError()
.errorType(ErrorType.NOT_FOUND)
.message(ex.getMessage())
.build();
}
}