In this post, we’ll implement a sample CRUD GraphQL API that uses Spring Boot and Spring Data JPA with PostgreSQL.
· Prerequisites
· Overview
∘ What is GraphQL?
∘ Why GraphQL?
· Getting Started
∘ Data entities Model
∘ Repositories interfaces
∘ GraphQLSource
∘ GraphQL Controller
· Test the API
· Testing
· Conclusion
· References
Prerequisites
This is the list of all the prerequisites for following this story:
- Spring Boot 3+
- Maven 3.6.3
- Java 21
- PostgreSQL
Overview
What is GraphQL?
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools. — https://graphql.org/
GraphQL was developed internally by Facebook in 2012 before being publicly released in 2015. On 7 November 2018, the GraphQL project was moved from Facebook to the newly established GraphQL Foundation, hosted by the non-profit Linux Foundation. Today, GraphQL is used by teams of all sizes in many different environments such as Facebook, Github, IBM, Airbnb, Shopify, etc.
Why GraphQL?
GraphQL was developed to meet the demand for greater flexibility and productivity. It provides solutions to many problems and inefficiencies encountered by developers when using REST APIs.
- Overfetching and Underfetching
Clients can obtain only the necessary data with GraphQL. neither less nor more. This resolves the problems brought on by overfetching and underfetching.
- No API versioning required
A single endpoint is typically used in GraphQL architecture. With GraphQL, there is no need to maintain versions. The one endpoint still facilitates data organization and can serve as a gateway and orchestrator for multiple data sources.
- Schema & Type System
GraphQL uses a strong type system to define the schema of an API. Types ensure that clients ask for only what is possible and provide
clear and helpful errors. All the types that are exposed in an API are written down in a schema using the GraphQL Schema Definition Language (SDL).
Getting Started
We will start by creating a simple Spring Boot project from start.spring.io, with the following dependencies: Spring Web, Spring for GraphQL, Spring Data JPA, Lombok, PostgreSQL Driver, and Validation.

Data entities Model
Let’s start by creating our entities. We have two entities: Author and Book.
One Author will have many Books, so it’s a One-to-Many relationship. These will be represented by tables in the PostgreSQL database.
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotBlank
private String lastname;
private String firstname;
@OneToMany(mappedBy = "author", fetch = FetchType.EAGER)
private Collection<Book> books;
}
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotBlank
@Size(max = 100)
private String title;
private int page;
private String isbn;
private String description;
private double price;
@ManyToOne
private Author author;
}
Repositories interfaces
In the repository package, we must create interfaces (AuthorRepository, BookRepository) that implement JpaRepository.
@Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {
}
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
}
GraphQLSource
GraphQlSource is a core Spring abstraction for access to the graphql.GraphQL instance to use for request execution. It provides a builder API to initialize GraphQL Java and build a GraphQlSource.
GraphQlSource.Builder can be configured with one or more Resource instances to be parsed and merged together. That means schema files can be loaded from just about any location. By default, the Spring Boot starter looks for schema files with extensions “.graphqls” or “.gqls” under the location classpath:graphql/**, which is typically src/main/resources/graphql.
Add a new file schema.graphqls to src/main/resources/graphql
schema {
query: Query
mutation: Mutation
}
type Query {
getBooks: [Book]
getAuthors: [Author]
getBook(id: ID): Book
getAuthor(id: ID): Author
}
type Mutation {
addBook(bookInput : BookInput) : Book
deleteBook(id :Int) :Int
updateBook(id:Int, bookInput:BookInput ) : Book
addAuthor(authorInput : AuthorInput) : Author
}
type Author {
id: ID!,
lastname : String!,
firstname : String,
books : [Book]
}
type Book {
id: ID!,
title : String!,
page : Int,
isbn : String,
description : String,
price : Float,
author : Author!
}
input AuthorInput {
lastname : String,
firstname : String
}
input BookInput {
title : String,
description : String,
isbn : String,
price : Float,
page : Int,
authorId : Int
}
GraphQL schemas are types of objects. Entities are usually mapped to object types. A type has fields that represent the data associated with each object.
The Author model will have fields to represent the information for an author with scalar fields: id, lastname, firstname, books.
Each field returns a specific type of data. The exclamation point “!” is used to return a non-null value for the field.
Query: It’s used to read data.
Mutation: It’s used to write operations create, update or delete.
Subscription: It allows us to listen to the GraphQL API for real-time data changes.
GraphQL Controller
Spring provides an annotation-based programming model where @Controller components use annotations to declare handler methods with flexible method signatures to fetch the data for specific GraphQL fields.
Let’s implement our Book controller:
@Controller
@Slf4j
public class BookGraphQLController {
private final AuthorService authorService;
private final BookService bookService;
public BookGraphQLController(AuthorService authorService, BookService bookService) {
this.authorService = authorService;
this.bookService = bookService;
}
@QueryMapping
public List<Book> getBooks(){
return bookService.getAll();
}
@QueryMapping
public List<Author> getAuthors(){
return authorService.getAll();
}
@QueryMapping
public Book getBook(@Argument long id){
return bookService.getOne(id);
}
@QueryMapping
public Author getAuthor(@Argument long id){
return authorService.getOne(id);
}
@MutationMapping
public Book addBook(@Argument @Valid BookInput bookInput){
Author author = authorService.getOne(bookInput.authorId());
Book book = EntityMapper.toBookEntity(bookInput);
book.setAuthor(author);
return bookService.create(book);
}
@MutationMapping
public Book updateBook(@Argument int id , @Argument @Valid BookInput bookInput){
Book book = bookService.getOne((long)id);
book.setPage(bookInput.page());
book.setPrice(bookInput.price());
book.setTitle(bookInput.title());
book.setIsbn(bookInput.isbn());
book.setDescription(bookInput.description());
return bookService.update(book);
}
@MutationMapping
public Author addAuthor(@Argument @Valid AuthorInput authorInput){
Author author = EntityMapper.toAuthorEntity(authorInput);
return authorService.create(author);
}
@MutationMapping
public int deleteBook(@Argument int id){
authorService.delete((long)id);
return id;
}
}
@QueryMapping: Bind this method to a query
@MutationMapping: Bind this method to a mutation
@SubscriptionMapping: Bind this method to a subcription
@BatchMapping: help solving the n+1 problem.
@SchemaMapping: maps a handler method to a field in the GraphQL schema and declares it to be the DataFetcher for that field.
@Argument: For access to a named field argument bound to a higher-level, typed Object.
@DataLoader: When you register a batch loading function for an entity, as explained in Batch Loading, you can access the DataLoader for the entity by declaring a method argument of type DataLoader and use it to load the entity
Test the API
Now, we can run our application and test it.

Go to http://localhost:8080/graphiql?path=/graphql to run the queries.
- Create Author

- Create Book

- Get All Book

- Get By book Id

Testing
Spring for GraphQL provides dedicated support for testing GraphQL requests over HTTP, WebSocket, and RSocket, as well as for testing directly against a server.
To make use of this, we add spring-graphql-test and h2in dependencies:
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
Let’s write a controller slice test for the BookGraphQLController.
@SpringBootTest
@AutoConfigureGraphQlTester
class BookGraphQLControllerTest {
@Autowired
private GraphQlTester graphQlTester;
@Autowired
private BookRepository bookRepository;
@Autowired
private AuthorRepository authorRepository;
@Sql("classpath:/sql/data.sql")
@Test
void shouldGetFirstBook() {
graphQlTester
.documentName("bookDetails")
.variable("id", 1)
.execute()
.path("getBook")
.matchesJson("""
{
"id": "1",
"title": "Demo Spring boot graphQL",
"page": 416,
"isbn": "X4J-5H8",
"description": "This book covers the basics of Spring Boot and graphQL",
"author": {
"id": "1",
"firstname": "Bree",
"lastname": "Nasim"
}
}
""");
}
@Test
void shouldCreateNewAuthor() {
int authorCount = authorRepository.findAll().size();
String document = """
mutation {
addAuthor(authorInput: {
lastname: "Pilot Pen",
firstname: "Gel-based ball point pen"
}) {
id
lastname
firstname
}
}
""";
graphQlTester.document(document)
.execute()
.path("addAuthor")
.entity(Author.class)
.satisfies(author -> {
assertNotNull(author.getId());
assertEquals("Gel-based ball point pen", author.getFirstname());
});
assertEquals(authorCount + 1, authorRepository.findAll().size());
}
}
GraphQlTester is a contract that declares a common workflow for testing GraphQL requests that is independent of the underlying transport. That means requests are tested with the same API no matter what the underlying transport, and anything transport specific is configured at build time.

Conclusion
In this story, we implemented a CRUD GraphQL API using Spring Boot and Spring Data JPA with PostgreSQL.
GraphQL offers many advantages over REST APIs, but the full move to GraphQL should be analyzed based on your specific project, team knowledge, and client applications. If your application requires a robust API with complex security, caching, and monitoring, you should go with REST or GRPC.
The complete source code is available on GitHub.
Last updated: April 2, 2025