GraphQL with Spring Boot

2 minute read

GraphQL lets clients request exactly the data they need — no over-fetching, no under-fetching. Spring Boot 3 has first-class GraphQL support.


Setup

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
spring:
  graphql:
    graphiql:
      enabled: true        # GraphiQL UI at /graphiql
    path: /graphql
    schema:
      locations: classpath:graphql/

GraphQL Schema

src/main/resources/graphql/schema.graphqls:

type Query {
    books: [Book!]!
    book(id: ID!): Book
    booksByAuthor(author: String!): [Book!]!
}

type Mutation {
    createBook(input: CreateBookInput!): Book!
    updateBook(id: ID!, input: UpdateBookInput!): Book!
    deleteBook(id: ID!): Boolean!
}

type Book {
    id: ID!
    title: String!
    author: Author!
    isbn: String
    price: Float
    reviews: [Review!]!
}

type Author {
    id: ID!
    name: String!
    books: [Book!]!
}

type Review {
    id: ID!
    rating: Int!
    comment: String
    reviewer: String!
}

input CreateBookInput {
    title: String!
    authorId: ID!
    isbn: String
    price: Float
}

input UpdateBookInput {
    title: String
    price: Float
}

Controllers

@Controller
public class BookGraphQLController {

    private final BookService bookService;

    @QueryMapping
    public List<Book> books() {
        return bookService.findAll();
    }

    @QueryMapping
    public Book book(@Argument Long id) {
        return bookService.findById(id);
    }

    @QueryMapping
    public List<Book> booksByAuthor(@Argument String author) {
        return bookService.findByAuthor(author);
    }

    @MutationMapping
    public Book createBook(@Argument CreateBookInput input) {
        return bookService.create(input);
    }

    @MutationMapping
    public Book updateBook(@Argument Long id, @Argument UpdateBookInput input) {
        return bookService.update(id, input);
    }

    @MutationMapping
    public boolean deleteBook(@Argument Long id) {
        bookService.delete(id);
        return true;
    }
}

Nested Resolvers

@Controller
public class BookGraphQLController {

    // Resolve the 'author' field on Book
    @SchemaMapping(typeName = "Book", field = "author")
    public Author author(Book book) {
        return authorService.findById(book.getAuthorId());
    }

    // Resolve 'reviews' field on Book
    @SchemaMapping(typeName = "Book", field = "reviews")
    public List<Review> reviews(Book book) {
        return reviewService.findByBookId(book.getId());
    }
}

DataLoader — Solving N+1

@Controller
public class BookGraphQLController {

    @BatchMapping
    public Map<Book, Author> author(List<Book> books) {
        // Single batch query instead of N queries
        List<Long> authorIds = books.stream()
            .map(Book::getAuthorId).distinct().toList();
        Map<Long, Author> authorsById = authorService.findByIds(authorIds).stream()
            .collect(Collectors.toMap(Author::getId, a -> a));

        return books.stream()
            .collect(Collectors.toMap(book -> book, book -> authorsById.get(book.getAuthorId())));
    }
}

GraphQL Queries

# Get only what you need
query {
  books {
    title
    price
  }
}

# Get book with author and reviews
query {
  book(id: 1) {
    title
    author {
      name
    }
    reviews {
      rating
      comment
    }
  }
}

# Create a book
mutation {
  createBook(input: {
    title: "Clean Code"
    authorId: 1
    isbn: "978-0132350884"
    price: 39.99
  }) {
    id
    title
  }
}

GraphQL vs REST

Feature REST GraphQL
Endpoints Multiple (/books, /authors) Single (/graphql)
Over-fetching Common ❌ Client picks fields
Under-fetching Multiple requests ❌ Single query
Caching Simple (HTTP caching) Complex
File upload Simple Complex
Best for CRUD, simple APIs Complex, nested data

Error Handling

@Controller
public class BookGraphQLController {

    @QueryMapping
    public Book book(@Argument Long id) {
        return bookService.findById(id);  // throws ResourceNotFoundException
    }
}

@Component
public class GraphQLExceptionResolver extends DataFetcherExceptionResolverAdapter {

    @Override
    protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
        if (ex instanceof ResourceNotFoundException) {
            return GraphqlErrorBuilder.newError(env)
                .message(ex.getMessage())
                .errorType(ErrorType.NOT_FOUND)
                .build();
        }
        return null;  // default handling
    }
}

Key Takeaways

  1. Spring Boot 3 has native GraphQL support — schema-first approach.
  2. @QueryMapping for queries, @MutationMapping for mutations.
  3. @SchemaMapping resolves nested fields; @BatchMapping prevents N+1.
  4. GraphQL excels at complex, nested data — REST is simpler for CRUD.
  5. Use GraphiQL (/graphiql) for interactive query testing.

Module Complete!

🎉 You’ve completed the Spring Boot module — from project setup to GraphQL. You now know how to build production-ready APIs with Spring Boot, including REST, databases, security, testing, Docker, and more.

Updated: