1. Introduction

GraphQL is a query and manipulation language for web APIs. One of the libraries that originated to make working with GraphQL more seamless is SPQR.

In this tutorial, we’ll learn the basics of GraphQL SPQR and see it in action in a simple Spring Boot project.

2. What Is GraphQL SPQR?

GraphQL is a well-known query language created by Facebook. At its core are schemas - files in which we define custom types and functions.

In the traditional approach, if we wanted to add GraphQL to our project, we would have to follow two steps. First, we’d have to add GraphQL schema files to the project. Secondly, we’d need to write respective Java POJOs representing each type from the schema. This means that we’d be maintaining the same information in two places: in the schema files and in the Java classes. Such an approach is error-prone and requires more effort in maintaining the project.

GraphQL Schema Publisher & Query Resolver, SPQR in short, originated in order to reduce the above problems - it simply generates GraphQL schemas from the annotated Java classes.

3. Introducing GraphQL SPQR with Spring Boot

To see SPQR in action, we’ll set up a simple service. We’re going to use Spring Boot GraphQL Starter and GraphQL SPQR.

3.1. Setup

Let’s start by adding the dependencies for SPQR and Spring Boot to our POM:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.leangen.graphql</groupId>
    <artifactId>spqr</artifactId>
    <version>0.11.2</version>
</dependency>

3.2. Writing the Model Book Class

Now that we’ve added the necessary dependencies, let’s create a simple Book class:

1
2
3
4
5
public class Book {
    private Integer id;
    private String author;
    private String title;
}

As we can see above, it doesn’t include any SPQR annotations. This can be very useful if we don’t own the source code but would like to benefit from this library.

3.3. Writing the BookService

In order to manage the collection of books, let’s create an IBookService interface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public interface IBookService {
    Book getBookWithTitle(String title);

    List<Book> getAllBooks();

    Book addBook(Book book);

    Book updateBook(Book book);

    boolean deleteBook(Book book);
}

Then, we’ll provide an implementation of our interface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Service
public class BookService implements IBookService {

    Set<Book> books = new HashSet<>();

    public Book getBookWithTitle(String title) {
        return books.stream()
            .filter(book -> book.getTitle()
                .equals(title))
            .findFirst()
            .orElse(null);
    }

    public List<Book> getAllBooks() {
        return books.stream()
            .collect(Collectors.toList());
    }

    public Book addBook(Book book) {
        books.add(book);
        return book;
    }

    public Book updateBook(Book book) {
        books.remove(book);
        books.add(book);
        return book;
    }

    public boolean deleteBook(Book book) {
        return books.remove(book);
    }
}

3.4. Exposing the Service with graphql-spqr

The only thing left is creating a resolver, which will expose GraphQL mutations and queries. To do that, we’ll use two important SPQR annotations - @GraphQLMutation and @GraphQLQuery :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Service
public class BookResolver {

    @Autowired
    IBookService bookService;

    @GraphQLQuery(name = "getBookWithTitle")
    public Book getBookWithTitle(@GraphQLArgument(name = "title") String title) {
        return bookService.getBookWithTitle(title);
    }

    @GraphQLQuery(name = "getAllBooks", description = "Get all books")
    public List<Book> getAllBooks() {
        return bookService.getAllBooks();
    }

    @GraphQLMutation(name = "addBook")
    public Book addBook(@GraphQLArgument(name = "newBook") Book book) {
        return bookService.addBook(book);
    }

    @GraphQLMutation(name = "updateBook")
    public Book updateBook(@GraphQLArgument(name = "modifiedBook") Book book) {
        return bookService.updateBook(book);
    }

    @GraphQLMutation(name = "deleteBook")
    public void deleteBook(@GraphQLArgument(name = "book") Book book) {
        bookService.deleteBook(book);
    }
}

If we don’t want to write @GraphQLArgument in every method and are satisfied with GraphQL parameters being named as the input parameters, we can compile the code with the -parameters argument.

3.5. Rest Controller

Finally, we’ll define a Spring @RestController. In order to expose the service with SPQR, we’ll configure the GraphQLSchema and GraphQL objects:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@RestController
public class GraphqlController {

    private final GraphQL graphQL;

    @Autowired
    public GraphqlController(BookResolver bookResolver) {
        GraphQLSchema schema = new GraphQLSchemaGenerator()
          .withBasePackages("com.baeldung")
          .withOperationsFromSingleton(bookResolver)
          .generate();
        this.graphQL = new GraphQL.Builder(schema)
          .build();
    }

It’s important to note that we have to register our BookResolver as a singleton .

The last task in our journey with SPQR is creating a /graphql endpoint. It’ll serve as a single point of contact with our service and will execute requested queries and mutations:

1
2
3
4
5
6
7
@PostMapping(value = "/graphql")
    public Map<String, Object> execute(@RequestBody Map<String, String> request, HttpServletRequest raw)
      throws GraphQLException {
        ExecutionResult result = graphQL.execute(request.get("query"));
        return result.getData();
    }
}

3.6. Result

We can check the results by inspecting the /graphql endpoint. For example, let’s retrieve all of the Book records by executing the following cURL command:

1
2
3
4
5
curl -g \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"query":"{getAllBooks {id author title }}"}' \
  http://localhost:8080/graphql

3.7. Test

Once we’re done with the configuration, we can test our project. We’ll use MockMvc to test our new endpoint and validate the responses. Let’s define the JUnit test and autowire the required services:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class GraphqlControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    BookService bookService;

    private static final String GRAPHQL_PATH = "/graphql";

    @Test
    public void givenNoBooks_whenReadAll_thenStatusIsOk() throws Exception {

        String getAllBooksQuery = "{ getAllBooks {id author title } }";

        this.mockMvc.perform(post(GRAPHQL_PATH).content(toJSON(getAllBooksQuery))
            .contentType(
                MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.getAllBooks").isEmpty());
    }

    @Test
    public void whenAddBook_thenStatusIsOk() throws Exception {

        String addBookMutation = "mutation { addBook(newBook: {id: 123, author: \"J.R.R. Tolkien\", "
            + "title: \"The Lord of the Rings\"}) { id author title } }";

        this.mockMvc.perform(post(GRAPHQL_PATH).content(toJSON(addBookMutation))
            .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.addBook.id").value("123"))
            .andExpect(jsonPath("$.addBook.author").value("J.R.R. Tolkien"))
            .andExpect(jsonPath("$.addBook.title").value("The Lord of the Rings"));
    }

    private String toJSON(String query) throws JSONException {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("query", query);
        return jsonObject.toString();
    }
}

4. Using GraphQL SPQR Spring Boot Starter

The team working on SPQR has created a Spring Boot starter, which makes using it even easier. Let’s check it out!

4.1. Setup

We’ll start with adding the spqr-spring-boot-starter to our POM:

1
2
3
4
5
<dependency>
    <groupId>io.leangen.graphql</groupId>
    <artifactId>graphql-spqr-spring-boot-starter</artifactId>
    <version>0.0.6</version>
</dependency>

4.2. BookService

Then, we need to add two modifications to our BookService . First of all, it has to be annotated with the @GraphQLApi annotation. In addition, every method we’d like to expose in our API has to have a respective annotation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Service
@GraphQLApi
public class BookService implements IBookService {

    Set<Book> books = new HashSet<>();

    @GraphQLQuery(name = "getBookWithTitle")
    public Book getBookWithTitle(@GraphQLArgument(name = "title") String title) {
        return books.stream()
            .filter(book -> book.getTitle()
                .equals(title))
            .findFirst()
            .orElse(null);
    }

    @GraphQLQuery(name = "getAllBooks", description = "Get all books")
    public List<com.baeldung.sprq.Book> getAllBooks() {
        return books.stream()
            .toList();
    }

    @GraphQLMutation(name = "addBook")
    public Book addBook(@GraphQLArgument(name = "newBook") Book book) {
        books.add(book);
        return book;
    }

    @GraphQLMutation(name = "updateBook")
    public Book updateBook(@GraphQLArgument(name = "modifiedBook") Book book) {
        books.remove(book);
        books.add(book);
        return book;
    }

    @GraphQLMutation(name = "deleteBook")
    public boolean deleteBook(@GraphQLArgument(name = "book") Book book) {
        return books.remove(book);
    }
}

As we can see, we basically moved the code from the BookResolver to the BookService . Additionally, we don’t need the GraphqlController class - a /graphql endpoint will be added automatically .

5. Summary

GraphQL is an exciting framework and an alternative to traditional RESTful endpoints. While offering a lot of flexibility, it can also add some tedious tasks such as maintaining schema files. SPQR aspires to make working with GraphQL easier and less error-prone.

In this article, we saw how to add SPQR to the existing POJOs and configure it to serve queries and mutations. Then, we saw a new endpoint in action in GraphiQL. Lastly, we tested our code using Spring’s MockMvc and JUnit.

As always, the sample code used here is available over on GitHub. Additionally, the code for the GraphQL Spring Boot starter kit is available over on GitHub.

Reference https://www.baeldung.com/spring-boot-graphql-spqr