GraphQL in Spring Boot Applications
Build GraphQL APIs with Spring Boot and Spring GraphQL
This guide covers implementing GraphQL APIs in Spring Boot applications using Spring GraphQL.
Video Tutorial
Learn more about Spring Boot GraphQL in this comprehensive video tutorial:
Prerequisites
<dependencies>
<!-- Spring GraphQL -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
Schema Definition
GraphQL Schema
# src/main/resources/graphql/schema.graphqls
type Query {
book(id: ID!): Book
books: [Book]!
author(id: ID!): Author
authors: [Author]!
}
type Mutation {
createBook(input: BookInput!): Book!
updateBook(id: ID!, input: BookInput!): Book!
deleteBook(id: ID!): Boolean!
}
type Book {
id: ID!
title: String!
pages: Int!
author: Author!
reviews: [Review]!
}
type Author {
id: ID!
name: String!
books: [Book]!
}
type Review {
id: ID!
rating: Int!
comment: String
book: Book!
}
input BookInput {
title: String!
pages: Int!
authorId: ID!
}
Controllers
Query Resolver
@Controller
public class BookQueryResolver {
private final BookService bookService;
private final AuthorService authorService;
@QueryMapping
public Book book(@Argument ID id) {
return bookService.getBook(id);
}
@QueryMapping
public List<Book> books() {
return bookService.getAllBooks();
}
@SchemaMapping
public Author author(Book book) {
return authorService.getAuthor(book.getAuthorId());
}
@SchemaMapping
public List<Review> reviews(Book book) {
return bookService.getBookReviews(book.getId());
}
}
Mutation Resolver
@Controller
public class BookMutationResolver {
private final BookService bookService;
@MutationMapping
public Book createBook(@Argument BookInput input) {
return bookService.createBook(input);
}
@MutationMapping
public Book updateBook(@Argument ID id, @Argument BookInput input) {
return bookService.updateBook(id, input);
}
@MutationMapping
public boolean deleteBook(@Argument ID id) {
return bookService.deleteBook(id);
}
}
Services
Book Service
@Service
@Transactional
public class BookService {
private final BookRepository bookRepository;
private final AuthorRepository authorRepository;
public Book createBook(BookInput input) {
Author author = authorRepository.findById(input.getAuthorId())
.orElseThrow(() -> new AuthorNotFoundException(input.getAuthorId()));
Book book = new Book();
book.setTitle(input.getTitle());
book.setPages(input.getPages());
book.setAuthor(author);
return bookRepository.save(book);
}
public Book updateBook(ID id, BookInput input) {
Book book = bookRepository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
Author author = authorRepository.findById(input.getAuthorId())
.orElseThrow(() -> new AuthorNotFoundException(input.getAuthorId()));
book.setTitle(input.getTitle());
book.setPages(input.getPages());
book.setAuthor(author);
return bookRepository.save(book);
}
public boolean deleteBook(ID id) {
if (bookRepository.existsById(id)) {
bookRepository.deleteById(id);
return true;
}
return false;
}
}
Error Handling
GraphQL Error Handler
GraphQLErrorHandler/ExceptionWhileDataFetching come from the older,
unrelated graphql-java-kickstart libraries. The spring-boot-starter-graphql
dependency declared above (Spring for GraphQL) instead uses
DataFetcherExceptionResolverAdapter, registered as a @Component and
picked up automatically:
@Component
public class BookNotFoundExceptionResolver extends DataFetcherExceptionResolverAdapter {
@Override
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
if (ex instanceof BookNotFoundException) {
return GraphqlErrorBuilder.newError(env)
.errorType(ErrorType.NOT_FOUND)
.message(ex.getMessage())
.extensions(Map.of("code", "BOOK_NOT_FOUND"))
.build();
}
return null;
}
}
Data Loaders
Batch Loading
Spring for GraphQL wires per-request DataLoaders through the
BatchLoaderRegistry bean it autoconfigures - a manually created
DataLoader @Bean is never consulted, so register the batch function
against the registry instead:
@Configuration
public class DataLoaderConfig {
public DataLoaderConfig(BatchLoaderRegistry registry, AuthorRepository authorRepository) {
registry.forTypePair(ID.class, Author.class)
.registerMappedBatchLoader((authorIds, env) ->
Flux.fromIterable(authorIds)
.collectList()
.flatMapMany(ids -> Flux.fromIterable(authorRepository.findAllById(ids)))
.collectMap(Author::getId));
}
}
Using Data Loader
@Controller
public class BookController {
@SchemaMapping
public CompletableFuture<Author> author(Book book,
DataLoader<ID, Author> authorLoader) {
return authorLoader.load(book.getAuthorId());
}
}
Security
GraphQL Security Configuration
// WebSecurityConfigurerAdapter was removed in Spring Security 5.7+ (the
// version bundled with Spring Boot 3) in favor of a SecurityFilterChain
// bean, and authorizeRequests()/antMatchers() were replaced by
// authorizeHttpRequests()/requestMatchers().
@Configuration
@EnableWebSecurity
public class GraphQLSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/graphql").authenticated()
.anyRequest().permitAll())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}
Field-Level Security
@Controller
public class SecuredBookController {
@PreAuthorize("hasRole('ADMIN')")
@MutationMapping
public Book createBook(@Argument BookInput input) {
return bookService.createBook(input);
}
@Secured("ROLE_USER")
@SchemaMapping
public List<Review> reviews(Book book) {
return bookService.getBookReviews(book.getId());
}
}
Testing
GraphQL Test Configuration
@SpringBootTest
@AutoConfigureGraphQlTester
class BookControllerTest {
@Autowired
private GraphQlTester graphQlTester;
@Test
void shouldGetBookById() {
String query = """
query {
book(id: "1") {
id
title
author {
name
}
}
}
""";
graphQlTester.document(query)
.execute()
.path("book.id")
.entity(String.class)
.isEqualTo("1")
.path("book.title")
.entity(String.class)
.isEqualTo("Test Book");
}
}
Monitoring and Metrics
GraphQL Metrics
@Component
public class GraphQLMetrics {
private final MeterRegistry registry;
public void recordQueryExecution(String operationName, long duration) {
Timer.builder("graphql.query.execution")
.tag("operation", operationName)
.register(registry)
.record(duration, TimeUnit.MILLISECONDS);
}
public void recordError(String operationName, String errorType) {
registry.counter("graphql.errors",
"operation", operationName,
"type", errorType).increment();
}
}
Best Practices
-
Schema Design
- Use meaningful types
- Implement proper pagination
- Define clear mutations
- Use input types
-
Performance
- Implement data loaders
- Use proper caching
- Optimize queries
- Monitor N+1 problems
-
Security
- Validate input
- Implement authentication
- Use field-level security
- Rate limit requests
-
Error Handling
- Define error types
- Provide meaningful messages
- Handle partial results
- Log errors properly
Common Patterns
- Pagination
type Query {
books(first: Int, after: String): BookConnection!
}
type BookConnection {
edges: [BookEdge!]!
pageInfo: PageInfo!
}
type BookEdge {
node: Book!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
@Controller
public class PaginatedBookController {
@QueryMapping
public BookConnection books(@Argument Integer first, @Argument String after) {
Pageable pageable = createPageable(first, after);
Page<Book> bookPage = bookService.getBooks(pageable);
return new BookConnection(
createEdges(bookPage.getContent()),
createPageInfo(bookPage)
);
}
}
- Filtering and Sorting
input BookFilter {
title: String
minPages: Int
maxPages: Int
authorId: ID
}
input BookSort {
field: BookSortField!
direction: SortDirection!
}
enum BookSortField {
TITLE
PAGES
CREATED_AT
}
enum SortDirection {
ASC
DESC
}
type Query {
books(filter: BookFilter, sort: BookSort): [Book!]!
}
@Controller
public class FilteredBookController {
@QueryMapping
public List<Book> books(@Argument BookFilter filter,
@Argument BookSort sort) {
Specification<Book> spec = createSpecification(filter);
Sort sorting = createSort(sort);
return bookRepository.findAll(spec, sorting);
}
}
Client Integration
Apollo Client Setup
import { ApolloClient, InMemoryCache } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:8080/graphql',
cache: new InMemoryCache(),
headers: {
'Authorization': `Bearer ${token}`
}
});
// Query
const GET_BOOK = gql`
query GetBook($id: ID!) {
book(id: $id) {
id
title
author {
name
}
reviews {
rating
comment
}
}
}
`;
// Mutation
const CREATE_BOOK = gql`
mutation CreateBook($input: BookInput!) {
createBook(input: $input) {
id
title
}
}
`;
Conclusion
Effective GraphQL implementation in Spring Boot requires:
- Proper schema design
- Efficient resolvers
- Performance optimization
- Security measures
- Error handling
For more Spring Boot topics, check out: