This Post is a documented setup of a Spring Boot GraphQL-Project. If you are looking for an even shorter way, you may want to check out the sample project on Github: github.com/felbit/spring-boot-graphql-demo.
As a mathematician I really like graphs and as a computer scientist my heart beats for types. So let's talk about typed graphs, shall we?
Table of contents
- What is GraphQL?
- Spring Boot Setup
- Adding PostgreSQL
- Adding GraphQL
- Adding a GraphQL Schema
- Adding a Model
- Adding GraphiQL
- Adding a Complex Field
- Adding Author Model, Repository, Query and Mutation
- Connecting Article and Author
- Bonus: Splitting the Graph
Preface
This post is partly a documentation for my future me to know how to get started with a GraphQL project in Spring Boot again, since I did invest some time to get the basic setup up and running. So the setup is key. The data in here is just sample data without any real world meaning. To have any data, we will sketch a boring old blog example. So expect blog posts and authors.
This post is not an article about Spring Boot, PostgreSQL or JPA. I will start a simple Spring Boot project and show all the code to follow along, but I will not dive deeper into any of the topics.
What is GraphQL?
REST is the go to tool for defining APIs today. It works perfectly as long as you follow a resource oriented programming pattern. That often includes that you know about the whereabouts of the queried data. If a client needs data from multiple resources, REST quickly falls apart and tends to over-deliver on the data end.
"GraphQL is the better REST", is stated on the tutorial page How to GraphQL. GraphQL is not an API, though. It is a query language for calling an API that is defined through typed schemas to be used by GraphQL. GraphQL also provides language independent specification for implementing a server that can handle the graph queries. So a GraphQL implementation is the server runtime that understands GraphQL queries.
Yada, yada ... show me the code! - Here is some:
### This is a simple example for a schema definition
# not a sample for the following implementation
# if you just scrolled up to find the schema,
# please scroll down again.
type Foo {
id: ID!
title: String
bar: Bar
}
type Bar {
id: ID!
name: String
}
type Query {
findAllFoos: [Foo]!
countFoos: Long!
countBars: Long!
}
type Mutation {
newFoo: Foo!
deleteFoo: Boolean!
newBar: Foo!
...
}
Literally the first thing we see is type. GraphQL schemas are typed by the GraphQL type system.
The syntax for fields is always the following:
<fieldName>: <Type>[!]
where the field name may be any name to your liking. It may be a good idea to find a descriptive field name since this is the key in the requesting queries. The type has to be a type in the schema's known type system. That may be primitives like Long or complex types like String or types you have defined yourself, like Foo in the example above. After all the exclamation mark defines that a return value is not null. It kind of is the opposite of Haskell's Maybe type (or Java's Optional).
Spring Boot Setup
The easiest way to start a fresh Spring Boot project is by utilising the Initializr. Choose a Maven Project, Java and whatever version of Spring Boot is currently the stable (by the time of this writing it's 2.1.4). Give your project a group and an artifact identifier and add the following dependencies: Web, JPA and PostgreSQL. If you are familiar with Gradle and / or Kotlin, you may also use these. The proceeding concepts should not differ.
You click generate project, choose a location, unzip and open the project with the editor or IDE of your choice. If you try to run the project as it is you will get a database error. That does make sense since we haven't configured any database connection, yet. Let's to that!
Adding PostgreSQL
Let's add a database to your Postgres installation. If you have none on your system, here is a good source to get you started.
postgres=# CREATE ROLE sbgraphql WITH PASSWORD 'sbgraphql';
postgres=# CREATE DATABASE sbgraphql_demo OWNER sbgraphql;
Within your project's src/main/resources you find your application.properties file. In there add the following to connect to your Postgres database:
## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url=jdbc:postgresql://localhost:5432/sbgraphql_demo
spring.datasource.username=sbgraphql
spring.datasource.password=sbgraphql
spring.jpa.hibernate.ddl-auto=update
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
Hit that "run" button again and you should be rewarded with something like the following:
Started GraphqlDemoApplication in 4.839 seconds
Yeah, we have a Spring Boot application with JPA and PostgreSQL up and running! Ok, enough of the small talk - let's get that GraphQL up.
Adding GraphQL
If you have ever worked with a Spring Boot application before, adding GraphQL to your project is straight forward. Just add the com.graphql-java dependencies graphql-java-tools and graphql-spring-boot-starter to your Maven (or Gradle) configuration.
To do that, open the pom.xml file in your project's root directory and add the following lines to the dependencies section:
<!-- ... -->
<dependencies>
<!-- ... -->
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>5.2.4</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>5.0.2</version>
</dependency>
<!-- ... -->
</dependencies>
<!-- ... -->
Your version numbers may vary. Reimport your project dependencies and restart the server. Nothing should break at this point (since we actually haven't written any code yet). So, let's break things.
Adding a GraphQL Schema
The schema is essentially the API definition. It defines, which queries your backend understands. This is written in singular since there is only one graph. We will split the graph in parts and store the parts in multiple files but there must be one root to the graph and one root only.
We start by adding a schema.graphqls file to the src/main/resources directory.
To get a simple blog running, some sort of article could be relevant. So let's add an article definition:
type Article {
id: ID!
title: String!
text: String!
}
This adds a type Article to our schema definition. Like String, Long or ID, Article now becomes part of the type system of our GraphQL schema. Reminder: The exclamation mark (!) after the type declaration indicates that the return value is not null.
Ok, fun. But how do I get the articles from my backend?
type Query {
findAllArticles: [Article]!
}
You just defined your first query. Congratulations! By the way: there is only one query. All further query definitions will extend this query. That does make sense in the context of a Type. Query is just a type like String or Long or Article.
But wait! Why is there an exclamation mark after the result? What happens, if there is no article, yet? Right: you'll get an empty list back. Hence, you'll always get a list, never null.
Fun, again. But what if I just need one article? Do you know the id? Then let's define a query to get an article by its id:
type Query {
article(id: ID!): Article
findAllArticles: [Article]!
}
No exclamation mark at the end here - since the requested article may not be there. But you must provide an id here.
Another nice feature would be, to add articles to your blog, wouldn't it? Here you go:
type Mutation {
newArticle(title: String!, text: String!): Article!
}
Oh, a new Type: Mutation. As a Query type is intended to deliver information about your current state of the world, the Mutation type is intended to - well - mutate the state. So you now know enough, to finalise your article CRUD:
type Article {
id: ID!
title: String!
text: String!
}
type Query {
article(id: ID!): Article
findAllArticles: [Article]!
}
type Mutation {
newArticle(title: String!, text: String!): Article!
updateArticle(id: ID!, title: String!, text: String!): Article
deleteArticle(id: ID): Boolean!
}
Ok, fun and games. But where does the data come from?
Adding a Model
To map our API graph to a database we have to provide a data model and a resolver class for each query type. Adding a model is straight forward Spring Boot. In the src/main/java/<yourpackagename> package create a model package and therein an entity class Article:
package de.sandstorm.springboot.graphqldemo.model;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Getter @Setter @NoArgsConstructor
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String title;
private String text;
public Article(String title, String text) {
this.title = title;
this.text = text;
}
}
Add constructors and getters and setters or - my preferred way - use the project lombok library to handle that for you. I will not cover lombok in this tutorial, you can head over to Baeldung and get a proper introduction there.
Next we need a repository for our model data. That is easy thanks to Spring Boot. In the repository package create an Interface like this:
package de.sandstorm.springboot.graphqldemo.repository;
import de.sandstorm.springboot.graphqldemo.model.Article;
import org.springframework.data.repository.CrudRepository;
public interface ArticleRepository extends CrudRepository<Article, Long> { }
Done.
Next we need a resolver for the GraphQL query. Create a package named resolver and add the Java class Query there:
package de.sandstorm.springboot.graphqldemo.resolver;
import com.coxautodev.graphql.tools.GraphQLResolver;
import de.sandstorm.springboot.graphqldemo.model.Article;
import de.sandstorm.springboot.graphqldemo.repository.ArticleRepository;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class Query implements GraphQLResolver {
private ArticleRepository articleRepository;
public Query(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
public Optional<Article> article(Long id) {
return articleRepository.findById(id);
}
public Iterable<Article> findAllArticles() {
return articleRepository.findAll();
}
}
Now we need a second resolver for our Mutation type. That's analogue to the Query resolver. In the resolver package add a class called Mutation with the following content:
package de.sandstorm.springboot.graphqldemo.resolver;
import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import de.sandstorm.springboot.graphqldemo.model.Article;
import de.sandstorm.springboot.graphqldemo.repository.ArticleRepository;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class Mutation implements GraphQLMutationResolver {
private ArticleRepository articleRepository;
public Mutation(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
public Article newArticle(String title, String text) {
Article article = new Article();
article.setTitle(title);
article.setText(text);
articleRepository.save(article);
return article;
}
public Optional<Article> updateArticle(Long id, String title, String text) {
Optional<Article> article = articleRepository.findById(id);
article.ifPresent(a -> {
a.setTitle(title);
a.setText(text);
articleRepository.save(a);
});
return article;
}
public boolean deleteArticle(Long id) {
articleRepository.deleteById(id);
return true;
}
}
At this point we have a fully working GraphQL backend. All queries of the Type Query and Mutation are fully functional and we can operate on the Article type.
Adding GraphiQL
Since we don't have any frontend client at that point, it is not that easy to verify, that our queries actually work as intended. GraphiQL comes to the rescue. It is similar to the famous swagger in that sense, that it lets us explore our API in a browser.
GraphiQL is added through your pom.xml file as follows:
<!-- ... -->
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphiql-spring-boot-starter</artifactId>
<version>5.0.2</version>
</dependency>
<!-- ... -->
Reload your dependencies, restart your application and navigate to http://localhost:8080/graphiql and try your API:
Fancy, huh? What would be even more fancy? Having an author to our article, that would even be more fancy. So, let's add one - and with that, a bit of complexity.
Adding a Complex Field
Adding the Author type is straight forward:
type Author {
id: ID!
first_name: String
last_name: String!
}
And with that some queries and mutations:
type Query {
# ...
author(id: ID!): Author
findAllAuthors: [Author]!
}
type Mutation {
newArticle(title: String!, text: String!, authorId: ID!): Article!
updateArticle(id: ID!, title: String!, text: String!, authorId: ID!): Article
deleteArticle(id: ID): Boolean!
newAuthor(firstName: String!, lastName: String!): Author!
updateAuthor(id: ID!, firstName: String!, lastName: String!): Author
deleteAuthor(id: ID): Boolean!
}
It's important, that the additions are added to the existing type definitions. There is only one Query type and only one Mutation type.
By the way: There are more built-in types than just Query and Mutation. One example would be the Subscription type for reactive resources. But those are out of the scope of this tutorial. We will come back to that in another post in the future.
You have the API definition for the Author type. One thing is missing, though. We would like to connect Author and Articles. For that we add the Author type as a field to the Article type (and we don't want the author to be null):
type Article {
id: ID!
title: String!
text: String!
author: Author!
}
The API definition is complete.
Adding Author Model, Repository, Query and Mutation
Adding the author model in the model package is straight forward:
package de.sandstorm.springboot.graphqldemo.model;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
@Getter @Setter @NoArgsConstructor
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String firstName;
private String lastName;
public Author(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
And so is the author repository in the repository package:
package de.sandstorm.springboot.graphqldemo.repository;
import de.sandstorm.springboot.graphqldemo.model.Author;
import org.springframework.data.repository.CrudRepository;
public interface AuthorRepository extends CrudRepository<Author, Long> { }
The Query class contains no surprises:
package de.sandstorm.springboot.graphqldemo.resolver;
// import ...
import de.sandstorm.springboot.graphqldemo.repository.AuthorRepository;
@Component
public class Query implements GraphQLQueryResolver {
private ArticleRepository articleRepository;
private AuthorRepository authorRepository;
public Query(ArticleRepository articleRepository, AuthorRepository authorRepository) {
this.articleRepository = articleRepository;
this.authorRepository = authorRepository;
}
//...
public Optional<Author> author(Long id) {
return authorRepository.findById(id);
}
public Iterable<Author> findAllAuthors() {
return authorRepository.findAll();
}
}
Nor does the Mutation class:
package de.sandstorm.springboot.graphqldemo.resolver;
// import ...
import de.sandstorm.springboot.graphqldemo.model.Author;
import de.sandstorm.springboot.graphqldemo.repository.AuthorRepository;
@Component
public class Mutation implements GraphQLMutationResolver {
private ArticleRepository articleRepository;
private AuthorRepository authorRepository;
public Mutation(ArticleRepository articleRepository, AuthorRepository authorRepository) {
this.articleRepository = articleRepository;
this.authorRepository = authorRepository;
}
// ...
public Author newAuthor(String firstName, String lastName) {
Author author = new Author();
author.setFirstName(firstName);
author.setLastName(lastName);
authorRepository.save(author);
return author;
}
public Optional<Author> updateAuthor(Long id, String firstName, String lastName) {
Optional<Author> author = authorRepository.findById(id);
author.ifPresent(a -> {
a.setFirstName(firstName);
a.setLastName(lastName);
authorRepository.save(a);
});
return author;
}
public boolean deleteAuthor(Long id) {
authorRepository.deleteById(id);
return true;
}
}
If we had not connected the types Article and Author we would be done by now. But two steps are missing at this point to satisfy the connection: we have to add the Author to the Article class and we have to write a specific resolver for the connection between Article and Author.
Connecting Article and Author
Adding the author to the article class is nothing new and exciting:
package de.sandstorm.springboot.graphqldemo.model;
// import ...
import javax.persistence.*;
@Entity
@Getter @Setter @NoArgsConstructor
public class Article {
// ...
@ManyToOne
@JoinColumn(name = "author_id", nullable = false)
private Author author;
public Article(String title, String text, Author author) {
this.title = title;
this.text = text;
this.author = author;
}
}
The new things here are the annotations from javax.persistence, namely @ManyToOne and @JoinColumn. These are information regarding the JPA library.
For complex fields - like author here - getter and setter are not enough, though. We have to explicitly resolve the connection. Java-GraphQL provides an interface named GraphQLResolver to do exactly that. Inside the resolver package we add a new class ArticleResolver:
package de.sandstorm.springboot.graphqldemo.resolver;
import com.coxautodev.graphql.tools.GraphQLResolver;
import de.sandstorm.springboot.graphqldemo.model.Article;
import de.sandstorm.springboot.graphqldemo.model.Author;
import de.sandstorm.springboot.graphqldemo.repository.AuthorRepository;
import org.springframework.stereotype.Component;
@Component
public class ArticleResolver implements GraphQLResolver {
private AuthorRepository authorRepository;
public ArticleResolver(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
public Author getAuthor(Article article) {
return authorRepository.findById(article.getAuthor().getId()).orElse(null);
}
}
Restart your server and everything should work fine. Et voilà! Your GraphQL Project is ready to be extended!
Bonus: Splitting the Graph
I like to keep my graph nice and tidy. Therefore I tend to organise it into modules referencing the types. In src/main/resources add a directory named graphql. In there add two files, one for each type: Article.graphqls and Author.graphqls:
### Article.graphqls
type Article {
id: ID!
title: String!
text: String!
author: Author!
}
extend type Query {
article(id: ID!): Article
findAllArticles: [Article]!
}
extend type Mutation {
newArticle(title: String!, text: String!, authorId: ID!): Article!
updateArticle(id: ID!, title: String!, text: String!, authorId: ID!): Article
deleteArticle(id: ID): Boolean!
}
### Author.graphqls
type Author {
id: ID!
firstName: String
lastName: String!
}
extend type Query {
author(id: ID!): Author
findAllAuthors: [Author]!
}
extend type Mutation {
newAuthor(firstName: String!, lastName: String!): Author!
updateAuthor(id: ID!, firstName: String!, lastName: String!): Author
deleteAuthor(id: ID): Boolean!
}
That leaves us with a nice and tidy schema.graphqls:
type Query { }
type Mutation { }
You don't even need that. If you define the Author type as your graph root by deleting the "extend" keywords before Query and Mutation, you can delete the schema.graphqls entirely. But I prefer this setup.
You find all the code in a public repository at github.com/felbit/spring-boot-graphql-demo.