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

  1. Schema Design

    • Use meaningful types
    • Implement proper pagination
    • Define clear mutations
    • Use input types
  2. Performance

    • Implement data loaders
    • Use proper caching
    • Optimize queries
    • Monitor N+1 problems
  3. Security

    • Validate input
    • Implement authentication
    • Use field-level security
    • Rate limit requests
  4. Error Handling

    • Define error types
    • Provide meaningful messages
    • Handle partial results
    • Log errors properly

Common Patterns

  1. 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)
        );
    }
}
  1. 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: