GraphQL in Spring Boot Applications
Build GraphQL APIs with Spring Boot and Spring GraphQL
GraphQL in Spring Boot Applications
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
@Component
public class CustomGraphQLErrorHandler implements GraphQLErrorHandler {
@Override
public List<GraphQLError> processErrors(List<GraphQLError> errors) {
return errors.stream()
.map(this::processError)
.collect(Collectors.toList());
}
private GraphQLError processError(GraphQLError error) {
if (error instanceof ExceptionWhileDataFetching) {
ExceptionWhileDataFetching exceptionError = (ExceptionWhileDataFetching) error;
if (exceptionError.getException() instanceof BookNotFoundException) {
return new BookNotFoundError(exceptionError.getException().getMessage());
}
}
return error;
}
}
Custom Error Types
public class BookNotFoundError extends GraphQLError {
private final String message;
public BookNotFoundError(String message) {
this.message = message;
}
@Override
public String getMessage() {
return message;
}
@Override
public List<Object> getPath() {
return null;
}
@Override
public Map<String, Object> getExtensions() {
Map<String, Object> extensions = new HashMap<>();
extensions.put("code", "BOOK_NOT_FOUND");
extensions.put("classification", "DataFetchingException");
return extensions;
}
}
Data Loaders
Batch Loading
@Component
public class AuthorDataLoader implements BatchLoader<ID, Author> {
private final AuthorRepository authorRepository;
@Override
public CompletionStage<List<Author>> load(List<ID> authorIds) {
return CompletableFuture.supplyAsync(() ->
authorRepository.findAllById(authorIds));
}
}
@Configuration
public class DataLoaderConfig {
@Bean
public DataLoader<ID, Author> authorDataLoader(AuthorDataLoader loader) {
return DataLoaderFactory.newDataLoader(loader);
}
}
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
@Configuration
@EnableWebSecurity
public class GraphQLSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/graphql").authenticated()
.anyRequest().permitAll()
.and()
.oauth2ResourceServer()
.jwt();
}
}
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: