google-site-verification: google61fe8ba583a51912.html
GraphQL with Spring Boot: Complete Guide to Modern API Development

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:

  1. Efficient Data Fetching: Clients request only needed data
  2. Strong Typing: Compile-time validation through schema
  3. Single Endpoint: Simplified API structure
  4. Self-documenting: GraphiQL UI for exploration
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper