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

  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: