GraphQL is a query language for APIs that provides a complete and understandable description of the data in your API, giving clients the power to ask for exactly what they need.
1. GraphQL Overview and Setup
What is GraphQL?
- Query Language: Clients specify exactly what data they need
- Single Endpoint: One endpoint for all operations
- Strongly Typed: Schema defines available operations and types
- Self-documenting: Schema serves as documentation
Maven Dependencies
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>graphql-spring-boot</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<graphql.spring.version>19.0.0</graphql.spring.version>
<graphql.java.version>20.4</graphql.java.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- GraphQL Dependencies -->
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
<version>${graphql.spring.version}</version>
</dependency>
<!-- Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Tools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Application Configuration
# application.properties spring.application.name=graphql-spring-boot # GraphQL spring.graphql.path=/graphql spring.graphql.graphiql.enabled=true spring.graphql.schema.printer.enabled=true # H2 Database spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= # JPA spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true # Logging logging.level.org.springframework.graphql=DEBUG logging.level.graphql=DEBUG
2. Domain Model and Entities
// User Entity
package com.example.graphql.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Size(min = 2, max = 50)
@Column(nullable = false)
private String name;
@NotBlank
@Email
@Column(unique = true, nullable = false)
private String email;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UserRole role = UserRole.USER;
@Column(name = "created_at")
private LocalDateTime createdAt;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Post> posts = new ArrayList<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Comment> comments = new ArrayList<>();
// Constructors
public User() {
this.createdAt = LocalDateTime.now();
}
public User(String name, String email) {
this();
this.name = name;
this.email = email;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public UserRole getRole() { return role; }
public void setRole(UserRole role) { this.role = role; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public List<Post> getPosts() { return posts; }
public void setPosts(List<Post> posts) { this.posts = posts; }
public List<Comment> getComments() { return comments; }
public void setComments(List<Comment> comments) { this.comments = comments; }
}
// User Role Enum
enum UserRole {
USER, ADMIN, MODERATOR
}
// Post Entity
package com.example.graphql.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Size(min = 5, max = 200)
@Column(nullable = false)
private String title;
@NotBlank
@Size(min = 10, max = 5000)
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private PostStatus status = PostStatus.DRAFT;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id", nullable = false)
private User author;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Comment> comments = new ArrayList<>();
@ElementCollection
@CollectionTable(name = "post_tags", joinColumns = @JoinColumn(name = "post_id"))
@Column(name = "tag")
private List<String> tags = new ArrayList<>();
// Constructors
public Post() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public Post(String title, String content, User author) {
this();
this.title = title;
this.content = content;
this.author = author;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) {
this.title = title;
this.updatedAt = LocalDateTime.now();
}
public String getContent() { return content; }
public void setContent(String content) {
this.content = content;
this.updatedAt = LocalDateTime.now();
}
public PostStatus getStatus() { return status; }
public void setStatus(PostStatus status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public User getAuthor() { return author; }
public void setAuthor(User author) { this.author = author; }
public List<Comment> getComments() { return comments; }
public void setComments(List<Comment> comments) { this.comments = comments; }
public List<String> getTags() { return tags; }
public void setTags(List<String> tags) { this.tags = tags; }
public void addTag(String tag) {
if (this.tags == null) {
this.tags = new ArrayList<>();
}
this.tags.add(tag);
}
}
// Post Status Enum
enum PostStatus {
DRAFT, PUBLISHED, ARCHIVED
}
// Comment Entity
package com.example.graphql.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
@Entity
@Table(name = "comments")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Size(min = 1, max = 1000)
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
@Column(name = "created_at")
private LocalDateTime createdAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
// Constructors
public Comment() {
this.createdAt = LocalDateTime.now();
}
public Comment(String content, Post post, User user) {
this();
this.content = content;
this.post = post;
this.user = user;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public Post getPost() { return post; }
public void setPost(Post post) { this.post = post; }
public User getUser() { return user; }
public void setUser(User user) { this.user = user; }
}
3. Repository Layer
// User Repository
package com.example.graphql.repository;
import com.example.graphql.entity.User;
import com.example.graphql.entity.UserRole;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByRole(UserRole role);
List<User> findByNameContainingIgnoreCase(String name);
@Query("SELECT u FROM User u WHERE SIZE(u.posts) > :minPosts")
List<User> findActiveUsers(@Param("minPosts") int minPosts);
boolean existsByEmail(String email);
}
// Post Repository
package com.example.graphql.repository;
import com.example.graphql.entity.Post;
import com.example.graphql.entity.PostStatus;
import com.example.graphql.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
List<Post> findByStatus(PostStatus status);
List<Post> findByAuthor(User author);
List<Post> findByTitleContainingIgnoreCase(String title);
List<Post> findByTagsContaining(String tag);
@Query("SELECT p FROM Post p JOIN p.tags t WHERE t IN :tags")
List<Post> findByTagsIn(@Param("tags") List<String> tags);
@Query("SELECT p FROM Post p WHERE p.author.id = :authorId AND p.status = :status")
List<Post> findByAuthorIdAndStatus(@Param("authorId") Long authorId,
@Param("status") PostStatus status);
}
// Comment Repository
package com.example.graphql.repository;
import com.example.graphql.entity.Comment;
import com.example.graphql.entity.Post;
import com.example.graphql.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByPost(Post post);
List<Comment> findByUser(User user);
List<Comment> findByPostId(Long postId);
}
4. GraphQL Schema Definition
# src/main/resources/graphql/schema.graphqls
scalar LocalDateTime
type Query {
# User queries
users: [User!]!
user(id: ID!): User
userByEmail(email: String!): User
usersByRole(role: UserRole!): [User!]!
# Post queries
posts: [Post!]!
post(id: ID!): Post
postsByStatus(status: PostStatus!): [Post!]!
postsByAuthor(authorId: ID!): [Post!]!
searchPosts(title: String): [Post!]!
postsByTags(tags: [String!]!): [Post!]!
# Comment queries
commentsByPost(postId: ID!): [Comment!]!
commentsByUser(userId: ID!): [Comment!]!
}
type Mutation {
# User mutations
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
# Post mutations
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
publishPost(id: ID!): Post!
# Comment mutations
createComment(input: CreateCommentInput!): Comment!
updateComment(id: ID!, input: UpdateCommentInput!): Comment!
deleteComment(id: ID!): Boolean!
}
type Subscription {
postPublished: Post!
commentAdded(postId: ID!): Comment!
}
# Types
type User {
id: ID!
name: String!
email: String!
role: UserRole!
createdAt: LocalDateTime!
posts: [Post!]!
comments: [Comment!]!
postCount: Int!
}
type Post {
id: ID!
title: String!
content: String!
status: PostStatus!
createdAt: LocalDateTime!
updatedAt: LocalDateTime!
author: User!
comments: [Comment!]!
tags: [String!]!
commentCount: Int!
}
type Comment {
id: ID!
content: String!
createdAt: LocalDateTime!
post: Post!
user: User!
}
# Input Types
input CreateUserInput {
name: String!
email: String!
role: UserRole = USER
}
input UpdateUserInput {
name: String
email: String
role: UserRole
}
input CreatePostInput {
title: String!
content: String!
authorId: ID!
tags: [String!]
}
input UpdatePostInput {
title: String
content: String
status: PostStatus
tags: [String!]
}
input CreateCommentInput {
content: String!
postId: ID!
userId: ID!
}
input UpdateCommentInput {
content: String!
}
# Enums
enum UserRole {
USER
ADMIN
MODERATOR
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
5. GraphQL Resolvers (Controllers)
// User Resolver
package com.example.graphql.resolver;
import com.example.graphql.entity.User;
import com.example.graphql.entity.UserRole;
import com.example.graphql.service.UserService;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@Controller
public class UserResolver {
private final UserService userService;
public UserResolver(UserService userService) {
this.userService = userService;
}
// Query mappings
@QueryMapping
public List<User> users() {
return userService.findAllUsers();
}
@QueryMapping
public User user(@Argument Long id) {
return userService.findUserById(id);
}
@QueryMapping
public User userByEmail(@Argument String email) {
return userService.findUserByEmail(email);
}
@QueryMapping
public List<User> usersByRole(@Argument UserRole role) {
return userService.findUsersByRole(role);
}
// Mutation mappings
@MutationMapping
public User createUser(@Argument CreateUserInput input) {
return userService.createUser(input);
}
@MutationMapping
public User updateUser(@Argument Long id, @Argument UpdateUserInput input) {
return userService.updateUser(id, input);
}
@MutationMapping
public Boolean deleteUser(@Argument Long id) {
userService.deleteUser(id);
return true;
}
// Schema mappings for field resolvers
@SchemaMapping(typeName = "User", field = "postCount")
public Integer postCount(User user) {
return userService.getPostCount(user.getId());
}
// Input record classes
public record CreateUserInput(String name, String email, UserRole role) {}
public record UpdateUserInput(String name, String email, UserRole role) {}
}
// Post Resolver
package com.example.graphql.resolver;
import com.example.graphql.entity.Post;
import com.example.graphql.entity.PostStatus;
import com.example.graphql.service.PostService;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@Controller
public class PostResolver {
private final PostService postService;
public PostResolver(PostService postService) {
this.postService = postService;
}
// Query mappings
@QueryMapping
public List<Post> posts() {
return postService.findAllPosts();
}
@QueryMapping
public Post post(@Argument Long id) {
return postService.findPostById(id);
}
@QueryMapping
public List<Post> postsByStatus(@Argument PostStatus status) {
return postService.findPostsByStatus(status);
}
@QueryMapping
public List<Post> postsByAuthor(@Argument Long authorId) {
return postService.findPostsByAuthor(authorId);
}
@QueryMapping
public List<Post> searchPosts(@Argument String title) {
return postService.searchPostsByTitle(title);
}
@QueryMapping
public List<Post> postsByTags(@Argument List<String> tags) {
return postService.findPostsByTags(tags);
}
// Mutation mappings
@MutationMapping
public Post createPost(@Argument CreatePostInput input) {
return postService.createPost(input);
}
@MutationMapping
public Post updatePost(@Argument Long id, @Argument UpdatePostInput input) {
return postService.updatePost(id, input);
}
@MutationMapping
public Boolean deletePost(@Argument Long id) {
postService.deletePost(id);
return true;
}
@MutationMapping
public Post publishPost(@Argument Long id) {
return postService.publishPost(id);
}
// Schema mappings for field resolvers
@SchemaMapping(typeName = "Post", field = "commentCount")
public Integer commentCount(Post post) {
return postService.getCommentCount(post.getId());
}
// Input record classes
public record CreatePostInput(String title, String content, Long authorId, List<String> tags) {}
public record UpdatePostInput(String title, String content, PostStatus status, List<String> tags) {}
}
// Comment Resolver
package com.example.graphql.resolver;
import com.example.graphql.entity.Comment;
import com.example.graphql.service.CommentService;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@Controller
public class CommentResolver {
private final CommentService commentService;
public CommentResolver(CommentService commentService) {
this.commentService = commentService;
}
// Query mappings
@QueryMapping
public List<Comment> commentsByPost(@Argument Long postId) {
return commentService.findCommentsByPost(postId);
}
@QueryMapping
public List<Comment> commentsByUser(@Argument Long userId) {
return commentService.findCommentsByUser(userId);
}
// Mutation mappings
@MutationMapping
public Comment createComment(@Argument CreateCommentInput input) {
return commentService.createComment(input);
}
@MutationMapping
public Comment updateComment(@Argument Long id, @Argument UpdateCommentInput input) {
return commentService.updateComment(id, input);
}
@MutationMapping
public Boolean deleteComment(@Argument Long id) {
commentService.deleteComment(id);
return true;
}
// Input record classes
public record CreateCommentInput(String content, Long postId, Long userId) {}
public record UpdateCommentInput(String content) {}
}
6. Service Layer
// User Service
package com.example.graphql.service;
import com.example.graphql.entity.User;
import com.example.graphql.entity.UserRole;
import com.example.graphql.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> findAllUsers() {
return userRepository.findAll();
}
public User findUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found with id: " + id));
}
public User findUserByEmail(String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("User not found with email: " + email));
}
public List<User> findUsersByRole(UserRole role) {
return userRepository.findByRole(role);
}
public User createUser(UserResolver.CreateUserInput input) {
if (userRepository.existsByEmail(input.email())) {
throw new RuntimeException("User with email " + input.email() + " already exists");
}
User user = new User(input.name(), input.email());
if (input.role() != null) {
user.setRole(input.role());
}
return userRepository.save(user);
}
public User updateUser(Long id, UserResolver.UpdateUserInput input) {
User user = findUserById(id);
if (input.name() != null) {
user.setName(input.name());
}
if (input.email() != null && !input.email().equals(user.getEmail())) {
if (userRepository.existsByEmail(input.email())) {
throw new RuntimeException("User with email " + input.email() + " already exists");
}
user.setEmail(input.email());
}
if (input.role() != null) {
user.setRole(input.role());
}
return userRepository.save(user);
}
public void deleteUser(Long id) {
User user = findUserById(id);
userRepository.delete(user);
}
public Integer getPostCount(Long userId) {
User user = findUserById(userId);
return user.getPosts().size();
}
}
// Post Service
package com.example.graphql.service;
import com.example.graphql.entity.Post;
import com.example.graphql.entity.PostStatus;
import com.example.graphql.entity.User;
import com.example.graphql.repository.PostRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
public class PostService {
private final PostRepository postRepository;
private final UserService userService;
public PostService(PostRepository postRepository, UserService userService) {
this.postRepository = postRepository;
this.userService = userService;
}
public List<Post> findAllPosts() {
return postRepository.findAll();
}
public Post findPostById(Long id) {
return postRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Post not found with id: " + id));
}
public List<Post> findPostsByStatus(PostStatus status) {
return postRepository.findByStatus(status);
}
public List<Post> findPostsByAuthor(Long authorId) {
User author = userService.findUserById(authorId);
return postRepository.findByAuthor(author);
}
public List<Post> searchPostsByTitle(String title) {
if (title == null || title.trim().isEmpty()) {
return findAllPosts();
}
return postRepository.findByTitleContainingIgnoreCase(title);
}
public List<Post> findPostsByTags(List<String> tags) {
return postRepository.findByTagsIn(tags);
}
public Post createPost(PostResolver.CreatePostInput input) {
User author = userService.findUserById(input.authorId());
Post post = new Post(input.title(), input.content(), author);
if (input.tags() != null) {
post.setTags(input.tags());
}
return postRepository.save(post);
}
public Post updatePost(Long id, PostResolver.UpdatePostInput input) {
Post post = findPostById(id);
if (input.title() != null) {
post.setTitle(input.title());
}
if (input.content() != null) {
post.setContent(input.content());
}
if (input.status() != null) {
post.setStatus(input.status());
}
if (input.tags() != null) {
post.setTags(input.tags());
}
return postRepository.save(post);
}
public void deletePost(Long id) {
Post post = findPostById(id);
postRepository.delete(post);
}
public Post publishPost(Long id) {
Post post = findPostById(id);
post.setStatus(PostStatus.PUBLISHED);
return postRepository.save(post);
}
public Integer getCommentCount(Long postId) {
Post post = findPostById(postId);
return post.getComments().size();
}
}
// Comment Service
package com.example.graphql.service;
import com.example.graphql.entity.Comment;
import com.example.graphql.entity.Post;
import com.example.graphql.entity.User;
import com.example.graphql.repository.CommentRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
public class CommentService {
private final CommentRepository commentRepository;
private final PostService postService;
private final UserService userService;
public CommentService(CommentRepository commentRepository,
PostService postService,
UserService userService) {
this.commentRepository = commentRepository;
this.postService = postService;
this.userService = userService;
}
public List<Comment> findCommentsByPost(Long postId) {
Post post = postService.findPostById(postId);
return commentRepository.findByPost(post);
}
public List<Comment> findCommentsByUser(Long userId) {
User user = userService.findUserById(userId);
return commentRepository.findByUser(user);
}
public Comment createComment(CommentResolver.CreateCommentInput input) {
Post post = postService.findPostById(input.postId());
User user = userService.findUserById(input.userId());
Comment comment = new Comment(input.content(), post, user);
return commentRepository.save(comment);
}
public Comment updateComment(Long id, CommentResolver.UpdateCommentInput input) {
Comment comment = commentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Comment not found with id: " + id));
comment.setContent(input.content());
return commentRepository.save(comment);
}
public void deleteComment(Long id) {
Comment comment = commentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Comment not found with id: " + id));
commentRepository.delete(comment);
}
}
7. Custom Scalar for LocalDateTime
// LocalDateTime Scalar Configuration
package com.example.graphql.config;
import graphql.schema.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Configuration
public class GraphQLConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return wiringBuilder -> wiringBuilder
.scalar(localDateTimeScalar())
.build();
}
@Bean
public GraphQLScalarType localDateTimeScalar() {
return GraphQLScalarType.newScalar()
.name("LocalDateTime")
.description("Java 8 LocalDateTime scalar")
.coercing(new Coercing<LocalDateTime, String>() {
@Override
public String serialize(Object dataFetcherResult) throws CoercingSerializeException {
if (dataFetcherResult instanceof LocalDateTime) {
return ((LocalDateTime) dataFetcherResult)
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
throw new CoercingSerializeException("Expected LocalDateTime object");
}
@Override
public LocalDateTime parseValue(Object input) throws CoercingParseValueException {
if (input instanceof String) {
return LocalDateTime.parse((String) input, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
throw new CoercingParseValueException("Expected LocalDateTime string");
}
@Override
public LocalDateTime parseLiteral(Object input) throws CoercingParseLiteralException {
if (input instanceof StringValue) {
return LocalDateTime.parse(((StringValue) input).getValue(),
DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
throw new CoercingParseLiteralException("Expected LocalDateTime string literal");
}
})
.build();
}
}
8. Data Initialization
// Data Initializer
package com.example.graphql.config;
import com.example.graphql.entity.User;
import com.example.graphql.entity.UserRole;
import com.example.graphql.repository.UserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class DataInitializer implements CommandLineRunner {
private final UserRepository userRepository;
public DataInitializer(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public void run(String... args) throws Exception {
// Create sample users
if (userRepository.count() == 0) {
User admin = new User("Admin User", "[email protected]");
admin.setRole(UserRole.ADMIN);
userRepository.save(admin);
User moderator = new User("Moderator User", "[email protected]");
moderator.setRole(UserRole.MODERATOR);
userRepository.save(moderator);
User user1 = new User("John Doe", "[email protected]");
userRepository.save(user1);
User user2 = new User("Jane Smith", "[email protected]");
userRepository.save(user2);
System.out.println("Sample data initialized");
}
}
}
9. Error Handling
// Global Exception Handler
package com.example.graphql.exception;
import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.stereotype.Component;
@Component
public class GraphQLExceptionHandler extends DataFetcherExceptionResolverAdapter {
@Override
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
if (ex instanceof RuntimeException) {
return GraphqlErrorBuilder.newError()
.errorType(ErrorType.BAD_REQUEST)
.message(ex.getMessage())
.path(env.getExecutionStepInfo().getPath())
.location(env.getField().getSourceLocation())
.build();
}
return GraphqlErrorBuilder.newError()
.errorType(ErrorType.INTERNAL_ERROR)
.message("An unexpected error occurred")
.path(env.getExecutionStepInfo().getPath())
.location(env.getField().getSourceLocation())
.build();
}
}
10. Testing
// GraphQL Test Configuration
package com.example.graphql;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.graphql.test.tester.GraphQlTester;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.client.MockMvcWebTestClient;
import org.springframework.web.context.WebApplicationContext;
@TestConfiguration
public class GraphQLTestConfig {
@Bean
public GraphQlTester graphQlTester(WebApplicationContext context) {
WebTestClient client = MockMvcWebTestClient.bindToApplicationContext(context)
.configureClient()
.baseUrl("/graphql")
.build();
return HttpGraphQlTester.create(client);
}
}
// User Resolver Test
package com.example.graphql.resolver;
import com.example.graphql.entity.User;
import com.example.graphql.entity.UserRole;
import com.example.graphql.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.graphql.GraphQlTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.graphql.test.tester.GraphQlTester;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@GraphQlTest(UserResolver.class)
@Import(GraphQLTestConfig.class)
class UserResolverTest {
@Autowired
private GraphQlTester graphQlTester;
@MockBean
private UserService userService;
@Test
void shouldGetAllUsers() {
// Given
User user1 = new User("John Doe", "[email protected]");
user1.setId(1L);
User user2 = new User("Jane Smith", "[email protected]");
user2.setId(2L);
when(userService.findAllUsers()).thenReturn(List.of(user1, user2));
// When & Then
String query = """
query {
users {
id
name
email
}
}
""";
graphQlTester.document(query)
.execute()
.path("users")
.entityList(User.class)
.hasSize(2)
.path("users[0].name")
.entity(String.class)
.isEqualTo("John Doe");
}
@Test
void shouldCreateUser() {
// Given
User user = new User("New User", "[email protected]");
user.setId(1L);
when(userService.createUser(any())).thenReturn(user);
// When & Then
String mutation = """
mutation {
createUser(input: {
name: "New User",
email: "[email protected]",
role: USER
}) {
id
name
email
role
}
}
""";
graphQlTester.document(mutation)
.execute()
.path("createUser.name")
.entity(String.class)
.isEqualTo("New User")
.path("createUser.email")
.entity(String.class)
.isEqualTo("[email protected]");
}
}
11. Example Queries and Mutations
Sample GraphQL Queries:
# Get all users with their posts
query GetAllUsers {
users {
id
name
email
posts {
id
title
status
}
postCount
}
}
# Get specific user with detailed information
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
role
createdAt
posts {
id
title
content
status
tags
commentCount
}
}
}
# Search and filter posts
query SearchPosts($title: String, $status: PostStatus) {
searchPosts(title: $title) {
id
title
content
status
author {
name
email
}
tags
commentCount
}
postsByStatus(status: $status) {
id
title
status
}
}
# Nested queries with relationships
query GetPostWithComments($postId: ID!) {
post(id: $postId) {
id
title
content
author {
name
email
}
comments {
id
content
user {
name
}
createdAt
}
}
}
Sample GraphQL Mutations:
# Create a new user
mutation CreateUser {
createUser(input: {
name: "Alice Johnson",
email: "[email protected]",
role: USER
}) {
id
name
email
role
createdAt
}
}
# Create a post
mutation CreatePost {
createPost(input: {
title: "Getting Started with GraphQL",
content: "GraphQL is a query language for APIs...",
authorId: 1,
tags: ["graphql", "api", "spring-boot"]
}) {
id
title
content
status
author {
name
}
tags
}
}
# Update a post
mutation UpdatePost {
updatePost(id: 1, input: {
title: "Updated Title",
status: PUBLISHED
}) {
id
title
status
updatedAt
}
}
# Create a comment
mutation CreateComment {
createComment(input: {
content: "Great post! Very informative.",
postId: 1,
userId: 2
}) {
id
content
user {
name
}
post {
title
}
}
}
12. Main Application Class
// Spring Boot Application
package com.example.graphql;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GraphQLSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(GraphQLSpringBootApplication.class, args);
}
}
Conclusion
Key Benefits of GraphQL with Spring Boot:
- Efficient Data Fetching: Clients request only needed data
- Strong Typing: Compile-time validation through schema
- Single Endpoint: Simplified API structure
- Self-documenting: GraphiQL UI for exploration
- Flexible Queries: Nested relationships in single request
Best Practices:
- Use meaningful field names in schema
- Implement proper error handling
- Use DataLoader for N+1 query problems
- Secure your GraphQL endpoint
- Monitor and log GraphQL operations
- Use pagination for large datasets
Next Steps:
- Add authentication and authorization
- Implement DataLoader for batch loading
- Add query complexity analysis
- Implement field-level security
- Add subscription support for real-time updates
- Set up monitoring and metrics
This complete GraphQL implementation provides a robust foundation for building modern, efficient APIs with Spring Boot and Java.