The Representational State Transfer (REST) architecture is the most popular way to expose data from a server through an API.
REST defines a set of principles that must be followed. There's a certain degree of freedom, but the clients have to be adapted to the way in which the resources are represented on the server side, which makes REST not a good fit for some cases.
GraphQL is a query language that offers an alternative model to developing APIs using a strong type system to provide a detailed description of an API.
In this tutorial, you're going to learn how to build a GraphQL server that will expose an API to create, update and delete entities of type Book
with the following attributes:
Author
will be a complex type with the following attributes:
You'll define these types in a GraphQL schema using a special Graphql DSL called IDL. You'll also create resolvers, queries, mutations, error handlers and the rest of the infrastructure for the server.
This tutorial will use:
At the end of the tutorial, you'll have a strong foundation to build APIs with GraphQL using Spring Boot.
You can find the entire source code of this project on this GitHub repository.
Let's start by talking about GraphQL.
GraphQL is a query language for APIs created by Facebook in 2012, open-sourced in 2015, and now released as a specification.
GraphQL describes the data offered by an API through a schema that contains:
A set of operations:
For example, the following snippet defines the types Song
and Artist
along with their attributes and relationship between them:
1type Song {
2 id: ID!
3 title: String!
4 duration: Int!
5 genre: String!
6 artist: Artist!
7}
8
9type Artist {
10 id: ID!
11 name: String!
12 country: String!
13 songs: [Song]
14}
Notice that the fields are typed. These types can be scalar types (Int
, Float
, String
, Boolean
and ID
) or references to other types defined in the specification.
You can also specify if they are required (!
) or if they are an array ([]
). Here you can find more information about object types and fields.
Query operations are also treated as types. They declare fields that represent the available operations. For example, you can define query operations to get all the songs and filter the songs by genre:
1type SongQueries {
2 allSongs: [Song]
3 filterSongsByGenre(genre: String!): [Song]
4}
5
6schema {
7 query: SongQueries
8}
By specifying a return type, GraphQL allows you to request only the information you need from a resource. Here's a sample query to get the name of all the rock songs:
1{
2 filterSongsByGenre(genre: "rock") {
3 title
4 }
5}
And even request the information of related resources:
1{
2 filterSongsByGenre(genre: "rock") {
3 title
4 artist {
5 name
6 country
7 }
8 }
9}
These queries are typically sent over HTTP to a single server endpoint (unlike a REST architecture, where there are different endpoints for different resources).
The response is typically sent using JSON. For example, here's a sample response to the first query:
1{
2 "data": {
3 "filterSongsByGenre": [
4 {"title": "Song #0" },
5 {"title": "Song #1" }
6 ]
7 }
8}
In a similar way, you can define mutations to perform modifications on these types (optionally defining Input
types):
1# ...
2
3input SongInput {
4 title: String!
5 genre: String!
6 duration: Int!
7 artistID: ID!
8}
9
10type SongMutations {
11 newSong(song: SongInput!): Song
12}
13
14schema {
15 query: SongQueries
16 mutation: SongMutations
17}
Here's a sample mutation based on the above declaration:
1mutation {
2 newSong(sond: {
3 title: "Song #2",
4 duration: 122,
5 genre: "hrock",
6 artistID: 1,
7 }) {
8 id
9 title
10 genre
11 artist {
12 name
13 }
14 }
15}
Beyond the convention of using mutations to modify data, an important distinction between queries and mutations is that queries are executed in parallel, while mutations are executed one after the other to avoid race conditions.
There's a lot more to learn about GraphQL, but these concepts (types, queries, and mutations) are the foundation.
Now let's see how Spring Boot can help us implement a GraphQL server in an easy way.
graphql-java is a Java library that implements the GraphQL specification.
You can add the library as a dependency on your project to start using it. At the time of writing this tutorial, the latest version is 6.0
:
1<!-- https://mvnrepository.com/artifact/com.graphql-java/graphql-java -->
2<dependency>
3 <groupId>com.graphql-java</groupId>
4 <artifactId>graphql-java</artifactId>
5 <version>6.0</version>
6</dependency>
However, you'll also want to use a library like GraphQL Java Tools to parse GraphQL schemas instead of describing your types programmatically. This library also maps them automatically to Java objects:
1<!-- https://mvnrepository.com/artifact/com.graphql-java/graphql-java-tools -->
2<dependency>
3 <groupId>com.graphql-java</groupId>
4 <artifactId>graphql-java-tools</artifactId>
5 <version>4.3.0</version>
6</dependency>
If you're building a web application, you'll also want to use GraphQL Servlet, which implements a servlet that supports GET
and POST
requests for GraphQL queries:
1<!-- https://mvnrepository.com/artifact/com.graphql-java/graphql-java-servlet -->
2<dependency>
3 <groupId>com.graphql-java</groupId>
4 <artifactId>graphql-java-servlet</artifactId>
5 <version>4.7.0</version>
6</dependency>
Integrating all these projects requires a lot of boilerplate code. Luckily, graphql-java
is supported by Spring Boot with the use of the graphql-spring-boot-starter project.
You just have to add graphql-spring-boot-starter
to your project:
1<!-- https://mvnrepository.com/artifact/com.graphql-java/graphql-spring-boot-starter -->
2<dependency>
3 <groupId>com.graphql-java</groupId>
4 <artifactId>graphql-spring-boot-starter</artifactId>
5 <version>3.10.0</version>
6</dependency>
This starter will add and autoconfigure a GraphQL Servlet at /graphql
and use a GraphQL schema library (like GraphQL Java Tools) to parse all the schema files found on the classpath.
Also, the following parameters will be available (via application.yml
or application.properties
) with these default values:
1graphql:
2 servlet:
3 mapping: /graphql
4 enabled: true
5 corsEnabled: true
However, at the time of this writing, graphql-spring-boot-starter
only works with Spring Boot 1.x, there's no support for Spring Boot 2 at the moment. According to this issue, support for Spring Boot 2 will be added when its general availability version is released (due by February 2018 at this moment).
Now let's bootstrap a Spring Boot application using Spring Initializr.
Go to https://start.spring.io/ and choose the following options:
com.example
(or any other identifier) as the GroupDemoGraphQL
(or any other identifier) as the ArtifactGenerate the project, unzip the downloaded file, and open the project in your favorite IDE.
I'm going to use Java 9 so in pom.xml
, let's start by changing the java version in the properties:
1<properties>
2 ...
3 <java.version>9</java.version>
4</properties>
At this time, Spring Boot 1.x has some compatibility issues with Java 9. So we're going to remove spring-boot-devtools
(if it's added to your project) and add the dependency jaxb-api
:
1<dependency>
2 <groupId>javax.xml.bind</groupId>
3 <artifactId>jaxb-api</artifactId>
4 <version>2.3.0</version>
5</dependency>
Next, add the graphql-spring-boot-starter
:
1<dependency>
2 <groupId>com.graphql-java</groupId>
3 <artifactId>graphql-spring-boot-starter</artifactId>
4 <version>3.10.0</version>
5</dependency>
It will add graphql-java
and graphql-java-servlet
as dependencies of your project, however, you still have to add a library to parse the GraphQL Schema. At this time, you have two options:
I'll add GraphQL Java Tools:
1<dependency>
2 <groupId>com.graphql-java</groupId>
3 <artifactId>graphql-java-tools</artifactId>
4 <version>4.3.0</version>
5</dependency>
For this tutorial, we're going to use an embedded H2 database, but since we removed spring-boot-devtools
, we have to enable an embedded H2 database explicitly in src/main/resources/application.resources
:
1spring.h2.console.enabled=true
2spring.h2.console.path=/h2-console
Update your Maven configuration in your IDE to download the dependencies if you haven't already, and run the main class of your project, in my case, com.example.DemoGraphQL
. Alternatively, in a terminal window, you can execute mvnw spring-boot:run
(or gradle bootRun
if you're using Gradle).
Apart from some warnings, everything should look fine in the console logs. You can go to http://localhost:8080/h2-console/login.jsp and enter the following information:
jdbc:h2:mem:testdb
sa
When you press the Connect button, the console of the in-memory database should appear:
If everything is fine, you're ready to start writing the GraphQL schema.
At this point, if you go to http://localhost:8080/graphql, you'll see an error because nothing has been defined. So let's start with the GraphQL schema.
As said before, we're going to use the DSL language to create the schema instead of the programmatic way.
These schemas are written in .graphqls
files, which can be placed anywhere in the classpath. So in src/main/resources/
create a graphql
directory and inside of it, create the files author.graphqls
and book.graphqls
:
You can split up your schema into more than one file to keep it organized. However, there can be only one root Query
and only one root Mutation
types that will contain all the query and mutation operations.
So in the file author.graphqls
, we're going to define the Author
type and the query and mutation operations we're going to implement for this type:
1type Author {
2 id: ID!
3 firstName: String!
4 lastName: String!
5}
6
7type Query {
8 findAllAuthors: [Author]!
9 countAuthors: Long!
10}
11
12type Mutation {
13 newAuthor(firstName: String!, lastName: String!) : Author!
14}
And in the file book.graphqls
, in addition to the Book
type, we're going to extend the Query
and Mutation
types:
1type Book {
2 id: ID!
3 title: String!
4 isbn: String!
5 pageCount: Int
6 author: Author
7}
8
9extend type Query {
10 findAllBooks: [Book]!
11 countBooks: Long!
12}
13
14extend type Mutation {
15 newBook(title: String!, isbn: String!, pageCount: Int, author: ID!) : Book!
16 deleteBook(id: ID!) : Boolean
17 updateBookPageCount(pageCount: Int!, id: ID!) : Book!
18}
This way, at runtime, the Query
type will contain all the author and book operation, as it was defined like:
1type Query {
2 findAllAuthors: [Author]!
3 countAuthors: Long!
4 findAllBooks: [Book]!
5 countBooks: Long!
6}
Just like the Mutation
type:
1type Mutation {
2 newAuthor(firstName: String!, lastName: String!) : Author!
3 newBook(title: String!, isbn: String!, pageCount: Int, author: ID!) : Book!
4 deleteBook(id: ID!) : Boolean
5 updateBookPageCount(pageCount: Int!, id: ID!) : Book!
6}
About the types you can assign to fields or to the return values of operations, graphql-java
supports the following types:
Book
or Author
types defined above)1interface Shape {
2 color: String!,
3 size: Int!
4}
5type Triangle implements Shape { }
6type Square implements Shape { }
7type Query {
8 searchByColor(color: String!): [Shape]!
9}
1type Triangle {
2 # fields ...
3}
4type Square {
5 # fields ...
6}
7union Shape = Triangle | Square
8type Query {
9 searchByColor(sizes: Int!): [Shape]!
10}
1input BookInput {
2 title: String!
3 genre: String!
4 duration: Int!
5 artistID: ID!
6}
1enum Shape {
2 TRIANGLE
3 SQUARE
4}
Once you have a type defined, the library GraphQL Java Tools will map it to a Java object.
So let's create the package model
and define an Author
class with the constructors, getters and setters, equals
, and hashCode
methods:
1import javax.persistence.*;
2
3@Entity
4public class Author {
5 @Id
6 @GeneratedValue(strategy=GenerationType.AUTO)
7 private Long id;
8
9 private String firstName;
10
11 private String lastName;
12
13 public Author() {
14 }
15
16 public Author(Long id) {
17 this.id = id;
18 }
19
20 public Author(String firstName, String lastName) {
21 this.firstName = firstName;
22 this.lastName = lastName;
23 }
24
25 public Long getId() {
26 return id;
27 }
28
29 public void setId(Long id) {
30 this.id = id;
31 }
32
33 public String getFirstName() {
34 return firstName;
35 }
36
37 public void setFirstName(String firstName) {
38 this.firstName = firstName;
39 }
40
41 public String getLastName() {
42 return lastName;
43 }
44
45 public void setLastName(String lastName) {
46 this.lastName = lastName;
47 }
48
49 @Override
50 public boolean equals(Object o) {
51 if (this == o) return true;
52 if (o == null || getClass() != o.getClass()) return false;
53
54 Author author = (Author) o;
55
56 return id.equals(author.id);
57 }
58
59 @Override
60 public int hashCode() {
61 return id.hashCode();
62 }
63
64 @Override
65 public String toString() {
66 return "Author{" +
67 "id=" + id +
68 ", firstName='" + firstName + '\'' +
69 ", lastName='" + lastName + '\'' +
70 '}';
71 }
72}
The same applies to the Book
class:
1import javax.persistence.*;
2
3@Entity
4public class Book {
5 @Id
6 @GeneratedValue(strategy=GenerationType.AUTO)
7 private Long id;
8
9 private String title;
10
11 private String isbn;
12
13 private int pageCount;
14
15 @ManyToOne
16 @JoinColumn(name = "author_id",
17 nullable = false, updatable = false)
18 private Author author;
19
20 public Book() {
21 }
22
23 public Book(String title, String isbn, int pageCount, Author author) {
24 this.title = title;
25 this.isbn = isbn;
26 this.pageCount = pageCount;
27 this.author = author;
28 }
29
30 public Long getId() {
31 return id;
32 }
33
34 public void setId(Long id) {
35 this.id = id;
36 }
37
38 public String getTitle() {
39 return title;
40 }
41
42 public void setTitle(String title) {
43 this.title = title;
44 }
45
46 public String getIsbn() {
47 return isbn;
48 }
49
50 public void setIsbn(String isbn) {
51 this.isbn = isbn;
52 }
53
54 public int getPageCount() {
55 return pageCount;
56 }
57
58 public void setPageCount(int pageCount) {
59 this.pageCount = pageCount;
60 }
61
62 public Author getAuthor() {
63 return author;
64 }
65
66 public void setAuthor(Author author) {
67 this.author = author;
68 }
69
70 @Override
71 public boolean equals(Object o) {
72 if (this == o) return true;
73 if (o == null || getClass() != o.getClass()) return false;
74
75 Book book = (Book) o;
76
77 return id.equals(book.id);
78 }
79
80 @Override
81 public int hashCode() {
82 return id.hashCode();
83 }
84
85 @Override
86 public String toString() {
87 return "Book{" +
88 "id=" + id +
89 ", title='" + title + '\'' +
90 ", isbn='" + isbn + '\'' +
91 ", pageCount=" + pageCount +
92 ", author=" + author +
93 '}';
94 }
95}
Notice that these classes are annotated @Entity
, so they will represent the database's tables.
For scalar fields, getter and setter methods will be enough. However, for fields with complex types (like author
in Book
), you have to use Resolver
objects to resolve the value of those fields.
Resolver
objects implement the interface GraphQLResolver
. Create the package resolver
, and inside of it, the resolver for the Book
class:
1public class BookResolver implements GraphQLResolver<Book> {
2 private AuthorRepository authorRepository;
3
4 public BookResolver(AuthorRepository authorRepository) {
5 this.authorRepository = authorRepository;
6 }
7
8 public Author getAuthor(Book book) {
9 return authorRepository.findOne(book.getAuthor().getId());
10 }
11}
GraphQL Java Tools works with four types of Resolver
classes:
GraphQLResolver<T>
to resolve complex types.GraphQLQueryResolver
to define the operations of the root Query
type.GraphQLMutationResolver
to define the operations of the root Mutation
type.GraphQLSubscriptionResolver
to define the operations of the root Subscription
type.Subscriptions allows you to subscribe to a reactive source. They won't be reviewed in this tutorial, but the repositories for the Author
and Book
entities, and the GraphQLQueryResolver
and GraphQLMutationResolver
classes will be in the next section.
Let's start by creating the repositories. Spring Boot makes it really easy to create CRUD repositories.
In the repository
package, create the interface AuthorRepository
:
1public interface AuthorRepository extends CrudRepository<Author, Long> { }
And the interface BookRepository
:
1public interface BookRepository extends CrudRepository<Book, Long> { }
Spring Boot will generate an implementation with methods to find, save, count and delete for the Author
and Book
entities.
This way, implementing a GraphQLQueryResolver
is straightforward. In the resolver
package, create the class Query
:
1public class Query implements GraphQLQueryResolver {
2 private BookRepository bookRepository;
3 private AuthorRepository authorRepository;
4
5 public Query(AuthorRepository authorRepository, BookRepository bookRepository) {
6 this.authorRepository = authorRepository;
7 this.bookRepository = bookRepository;
8 }
9
10 public Iterable<Book> findAllBooks() {
11 return bookRepository.findAll();
12 }
13
14 public Iterable<Author> findAllAuthors() {
15 return authorRepository.findAll();
16 }
17
18 public long countBooks() {
19 return bookRepository.count();
20 }
21 public long countAuthors() {
22 return authorRepository.count();
23 }
24}
Creating the Mutation
class requires a little more work but it's still straightforward. After injecting the author and book repositories in the constructor:
1public class Mutation implements GraphQLMutationResolver {
2 private BookRepository bookRepository;
3 private AuthorRepository authorRepository;
4
5 public Mutation(AuthorRepository authorRepository, BookRepository bookRepository) {
6 this.authorRepository = authorRepository;
7 this.bookRepository = bookRepository;
8 }
9}
Based on the GraphQL schema, you need to define the methods to create new authors and books, taking the declared parameters and saving the entities using the corresponding repositories:
1public class Mutation implements GraphQLMutationResolver {
2 // ...
3 public Author newAuthor(String firstName, String lastName) {
4 Author author = new Author();
5 author.setFirstName(firstName);
6 author.setLastName(lastName);
7
8 authorRepository.save(author);
9
10 return author;
11 }
12
13 public Book newBook(String title, String isbn, Integer pageCount, Long authorId) {
14 Book book = new Book();
15 book.setAuthor(new Author(authorId));
16 book.setTitle(title);
17 book.setIsbn(isbn);
18 book.setPageCount(pageCount != null ? pageCount : 0);
19
20 bookRepository.save(book);
21
22 return book;
23 }
24}
Notice that the GraphQL type ID
can be converted to the Java types String
, Integer
, and Long
and the parameters names of the method and the schema don't have to match. However, you have to take into account the number of parameters, their type, and whether they are optional or not.
Next, here's the delete method:
1public class Mutation implements GraphQLMutationResolver {
2 // ...
3
4 public boolean deleteBook(Long id) {
5 bookRepository.delete(id);
6 return true;
7 }
8}
And finally, the method to update the book's page count:
1public class Mutation implements GraphQLMutationResolver {
2 // ...
3
4 public Book updateBookPageCount(Integer pageCount, Long id) {
5 Book book = bookRepository.findOne(id);
6 book.setPageCount(pageCount);
7
8 bookRepository.save(book);
9
10 return book;
11 }
12}
However, there's something about this method is not quite right.
What if the ID passed as argument is an invalid one?
The method will throw a NullPointerException
when the book to be updated cannot be found. This will be translated to a Internal server error on the client-side.
But the thing is that by default, any unhandled exception on the server-side will reach the client as a generic Internal server error.
Take a look at the default GraphQL Servlet's error handler:
1public class DefaultGraphQLErrorHandler implements GraphQLErrorHandler {
2 // ...
3
4 @Override
5 public List<GraphQLError> processErrors(List<GraphQLError> errors) {
6 final List<GraphQLError> clientErrors = filterGraphQLErrors(errors);
7 if (clientErrors.size() < errors.size()) {
8
9 // Some errors were filtered out to hide implementation - put a generic error in place.
10 clientErrors.add(new GenericGraphQLError("Internal Server Error(s) while executing query"));
11
12 errors.stream()
13 .filter(error -> !isClientError(error))
14 .forEach(error -> {
15 if(error instanceof Throwable) {
16 log.error("Error executing query!", (Throwable) error);
17 } else {
18 log.error("Error executing query ({}): {}", error.getClass().getSimpleName(), error.getMessage());
19 }
20 });
21 }
22
23 return clientErrors;
24 }
25
26 // ...
27}
Only the client errors (for example, when you misspell the name of a field) are processed. The rest of the errors are treated as a generic error and then logged.
If you want the client to get the correct message, the first thing you need to do is create a custom exception that implements GraphQLError
. For example, a BookNotFoundException
class:
1public class BookNotFoundException extends RuntimeException implements GraphQLError {
2
3 private Map<String, Object> extensions = new HashMap<>();
4
5 public BookNotFoundException(String message, Long invalidBookId) {
6 super(message);
7 extensions.put("invalidBookId", invalidBookId);
8 }
9
10 @Override
11 public List<SourceLocation> getLocations() {
12 return null;
13 }
14
15 @Override
16 public Map<String, Object> getExtensions() {
17 return extensions;
18 }
19
20 @Override
21 public ErrorType getErrorType() {
22 return ErrorType.DataFetchingException;
23 }
24}
GraphQLError
provides a field called extensions to pass additional data to the error object send to the client. In this case, we'll use it to pass the invalid book ID.
This way, in the updateBookPageCount
method, if the book cannot be found, we throw this exception:
1public class Mutation implements GraphQLMutationResolver {
2 // ...
3
4 public Book updateBookPageCount(Integer pageCount, Long id) {
5 Book book = bookRepository.findOne(id);
6 if(book == null) {
7 throw new BookNotFoundException("The book to be updated was not found", id);
8 }
9 book.setPageCount(pageCount);
10
11 bookRepository.save(book);
12
13 return book;
14 }
15}
By using an exception as a GraphQLError, the client will receive the complete stack trace in addition to the exception message.
If you don't want this behavior, you can create an adapter class to hide the exception (which can be wrapped in an ExceptionWhileDataFetching
class):
1public class GraphQLErrorAdapter implements GraphQLError {
2
3 private GraphQLError error;
4
5 public GraphQLErrorAdapter(GraphQLError error) {
6 this.error = error;
7 }
8
9 @Override
10 public Map<String, Object> getExtensions() {
11 return error.getExtensions();
12 }
13
14 @Override
15 public List<SourceLocation> getLocations() {
16 return error.getLocations();
17 }
18
19 @Override
20 public ErrorType getErrorType() {
21 return error.getErrorType();
22 }
23
24 @Override
25 public List<Object> getPath() {
26 return error.getPath();
27 }
28
29 @Override
30 public Map<String, Object> toSpecification() {
31 return error.toSpecification();
32 }
33
34 @Override
35 public String getMessage() {
36 return (error instanceof ExceptionWhileDataFetching) ? ((ExceptionWhileDataFetching) error).getException().getMessage() : error.getMessage();
37 }
38}
So now the only thing missing is to redefine GraphQL's default error handler.
You can create a Spring @Configuration
class or use the main class of the application annotated with @SpringBootApplication
to create a bean of type GraphQLErrorHandler
to replace the default error handler implementation:
1@SpringBootApplication
2public class DemoGraphQlApplication {
3
4 public static void main(String[] args) {
5 SpringApplication.run(DemoGraphQlApplication.class, args);
6 }
7
8 @Bean
9 public GraphQLErrorHandler errorHandler() {
10 return new GraphQLErrorHandler() {
11 @Override
12 public List<GraphQLError> processErrors(List<GraphQLError> errors) {
13 List<GraphQLError> clientErrors = errors.stream()
14 .filter(this::isClientError)
15 .collect(Collectors.toList());
16
17 List<GraphQLError> serverErrors = errors.stream()
18 .filter(e -> !isClientError(e))
19 .map(GraphQLErrorAdapter::new)
20 .collect(Collectors.toList());
21
22 List<GraphQLError> e = new ArrayList<>();
23 e.addAll(clientErrors);
24 e.addAll(serverErrors);
25 return e;
26 }
27
28 protected boolean isClientError(GraphQLError error) {
29 return !(error instanceof ExceptionWhileDataFetching || error instanceof Throwable);
30 }
31 };
32 }
33}
If you don't mind showing the exception stack trace to your clients, you can just return the list of errors that the method processErrors
receives as an argument.
But in this case, we do. So we first collect the client errors as they are, then the server errors, converting them to the adapter type (GraphQLErrorAdapter), and finally returning a list of all the errors collected.
While we're at it, let's also declare as Spring beans the resolvers for the Book
, Query
, and Mutation
types:
1@SpringBootApplication
2public class DemoGraphQlApplication {
3 // ...
4
5 @Bean
6 public BookResolver authorResolver(AuthorRepository authorRepository) {
7 return new BookResolver(authorRepository);
8 }
9
10 @Bean
11 public Query query(AuthorRepository authorRepository, BookRepository bookRepository) {
12 return new Query(authorRepository, bookRepository);
13 }
14
15 @Bean
16 public Mutation mutation(AuthorRepository authorRepository, BookRepository bookRepository) {
17 return new Mutation(authorRepository, bookRepository);
18 }
19}
And if you want, with a CommandLineRunner
bean, you can insert some data into the database:
1@SpringBootApplication
2public class DemoGraphQlApplication {
3 // ...
4
5 @Bean
6 public CommandLineRunner demo(AuthorRepository authorRepository, BookRepository bookRepository) {
7 return (args) -> {
8 Author author = new Author("Herbert", "Schildt");
9 authorRepository.save(author);
10
11 bookRepository.save(new Book("Java: A Beginner's Guide, Sixth Edition", "0071809252", 728, author));
12 };
13 }
14}
And that's it. Let's test the application.
Run the application and go to http://localhost:8080/graphql/schema.json. You should see a description of the GraphQL schema, something like this:
1{
2 "data": {
3 "__schema": {
4 "queryType": {
5 "name": "Query"
6 },
7 "mutationType": {
8 "name": "Mutation"
9 },
10 "subscriptionType": null,
11 "types": [
12 {
13 "kind": "OBJECT",
14 "name": "Query",
15 "description": "",
16 "fields": [
17 {
18 "name": "findAllAuthors",
19 "description": "",
20 "args": [],
21 "type": {
22 "kind": "NON_NULL",
23 "name": null,
24 "ofType": {
25 "kind": "LIST",
26 "name": null,
27 "ofType": {
28 "kind": "OBJECT",
29 "name": "Author",
30 "ofType": null
31 }
32 }
33 },
34 "isDeprecated": false,
35 "deprecationReason": null
36 },
37 ....
38 ]
39 }
40 ]
41 }
42 }
43}
Now you have three options to test the GraphQL server.
For example, if you want to execute this query:
1{
2 findAllBooks {
3 id
4 title
5 }
6}
The request could be sent via HTTP POST as JSON in this way:
1{
2 "query":"{findAllBooks { id title } }"
3}
Here's how it looks in Postman:
You can also send a GET request URL encoded, so a query like:
1http://localhost:8080/graphql?query={findAllBooks{id title}}
It will have to be sent like:
1http://localhost:8080/graphql?query=%7BfindAllBooks%7Bid%20title%7D%7D
But as you can see, this could become a painful process when working with complex queries.
Luckily, we can use GraphiQL, an Electron app that you can install to test your GraphQL endpoint:
There's a documentation tab on the right where you can see all the queries and mutations available and the app even has autocompletion for types and fields:
If you don't want to install the app, there's a web version you can incorporate into your application.
There's a Spring Boot starter for this version. Just add the following dependency:
1<!-- https://mvnrepository.com/artifact/com.graphql-java/graphiql-spring-boot-starter -->
2<dependency>
3 <groupId>com.graphql-java</groupId>
4 <artifactId>graphiql-spring-boot-starter</artifactId>
5 <version>3.10.0</version>
6</dependency>
Save the changes and run the application again. Now go to http://localhost:8080/graphiql and execute a query, for example:
1{
2 findAllBooks {
3 id
4 isbn
5 title
6 pageCount
7 author {
8 firstName
9 lastName
10 }
11 }
12}
The result should be something like:
These are the available Spring Boot configuration parameters for GraphiQL with their default values:
1graphiql:
2 mapping: /graphiql
3 endpoint: /graphql
4 enabled: true
This means that if you change the endpoint where the GraphQL servlet is deployed, you'll also have to change the GraphiQL configuration (in application.properties
):
1graphql.servlet.mapping: /graphql2
2
3graphiql.mapping: /graphiql
4graphiql.endpoint: /graphql2
Otherwise, you'll get an error like the following:
1{
2 "timestamp": 1513702998134,
3 "status": 404,
4 "error": "Not Found",
5 "message": "No message available",
6 "path": "/graphql2"
7}
Now try some queries.
For example, to create a new book:
1mutation {
2 newBook(
3 title: "Java: The Complete Reference, Tenth Edition",
4 isbn: "1259589331",
5 author: 1) {
6 id title
7 }
8}
To update the page count of a book:
1mutation {
2 updateBookPageCount(pageCount: 1344, id: 2) {
3 id pageCount
4 }
5}
To delete a book:
1mutation {
2 deleteBook(id:2)
3}
You can also watch the modifications performed by these mutations on the database. And don't forget to try the case where there's no book to update:
1mutation {
2 updateBookPageCount(pageCount: 1344, id: 20) {
3 id pageCount
4 }
5}
GraphQL is a flexible and powerful alternative to REST for building APIs. Here are some points to remember:
There's a lot more to learn about GraphQL, of course. Here are some good resources:
You can find the entire source code of the project on GitHub.
Contact me if you have questions or problems. Thanks for reading.