What is GraphQL?
GraphQL is both a query language for APIs and a runtime for fulfilling these 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. It was developed and released by Facebook in 2015. In 2018 it was moved to the newly established GraphQL foundation. Basically GraphQL consists of a type system, query system and execution semantics, static type validation and type introspection. This blog will give a high level overview about the GraphQL schema definion as well as how a GraphQL schema definition can be introduced into SpringBoot and how it can be tested.
GraphQL schema definition
For a detailed overview of the GraphQL schema definition please refer to https://graphql.org/learn/schema/.
Scalar Types
Type | Description |
ID | The ID scalar type represents a unique identifier. It it serialized same way as a String |
String | A UTF-8 character sequence |
Int | A signed 32-bit integer |
Float | A signed double-precision floating point value |
Boolean | true or false |
Per default every type is nullable. If non-nullability is desired an exclamation mark must be added to end of each type (e.g. String!). Furthermore custom scalar types can be defined.
Enumeration types
Also called enums. They are a special kind of scalar that is restricted to a particular set.
List types
Type modifiers [] are used to indicate that a distinct field will return a list.
Object types
The most basic components of a GraphQL schema are object types, which just represents a kind of object you can fetch from your service, and what fields it has.
Root types
Three types are special within a GraphQL schema: query, mutation, subscription. Every GraphQL service has a query type and may or may not have a mutation or type subscription type. These types are the same as a regular object type but they are special because they define the entry point of every GraphQL query.
Interfaces
Like many type systems, GraphQL supports interfaces. An Interface is an abstract type that includes a certain set of fields.
Union types
Union types are very similar to interfaces, but they don’t get to sepcify any common fields between types.
Input types
Complex types which can be passed as arguments within queries, mutations, subscriptions.
Schema definition example
# object type
type Person {
# scalar type
id: ID! # non-nullable (!)
# scalar type
name: String!
age: Int!
# list type
posts: [Post!]
}
type Post {
id: ID!
title: String
text: String!
}
type InfoSuccess {
info: String!
}
type InfoFailed {
failed: String!
}
# union type
union Info = InfoSuccess | InfoFailed
# enum type
enum Taste {
SWEET, SOUR
}
# root type
type Query {
persons: [Person!]!
person(id: ID!): Person! # id can be parsed as argument
posts: [Post!]!
post(id: ID!): Post!
}
# input type
input AddPerson {
name: String!
age: Int!
}
# input type
input AddPost {
personId: Int!
title: String!
text: String
}
# root type
type Mutation {
addPerson(input: AddPerson!): Person! # input is passed as argument
addPost(input: AddPost!): Post!
}
# root type
type Subscription {
numbers(bound: Int!): Int!
}
Spring for GraphQL
Basics
Spring for GraphQL (https://docs.spring.io/spring-graphql/docs/current/reference/html/#overview) is the successor of the GraphQL Java Spring Project. It reached version 1.0 in May 2022. SpringBoot runs the GraphQL application achieving type introspection, static type validation and execution semantics. A simpliefied workflow of a GraphQL query is shown below:
To enable the GraphQL engine within SpringBoot (at least v2.7.x of SpringBoot is needed) it is sufficient to add following dependencies (in this example maven) to the project:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<!-- For testing -->
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<scope>test</scope>
</dependency>
Per default Spring for Graphql tries to lookup the schema definition at src/min/resources/graphql
. Spring for GraphQL comes with a very handy development tool called GraphiQL which is quite useful to support development and test GraphQL APIs. It can be activated by adapting the SpringBoot application.properties file:
Furthermore Spring for GraphQL provides serveral properties to be adapted, e.g. the standard GraphQL endpoint or the schema file extension:
spring.graphql.graphiql.enabled=true
spring.graphql.path=/graphql
spring.graphql.graphiql.path=/graphiql
spring.graphql.schema.file-extensions=.graphqls
spring.graphql.websocket.path=/graphqlws
...
To be able to handle GraphQL queries it is necessary to implement distinct handler methods. With regard to the example shown above a handler method is implemented fetching all persons toghether with their name and age. The class PersonController contains PersonRepository as well as the handler method:
@Controller
public class PersonController {
private final PersonRepository personRepository;
public PersonController(final PersonRepository personRepository) {
this.personRepository = personRepository;
}
// maps schema definitions query { persons } to handler
@QueryMapping
public Iterable<Person> persons() {
return personRepository.findAll();
}
...
}
Without definiton of any kind of @SchemaMapping (more info at https://docs.spring.io/spring-graphql/docs/current/reference/html/#overview) it is important for the GraphQL engine the name of the handler method to be the same as the method’s name defined within the schema to be able to establish the neccessary bindings. Using a mutation mapping new persons can be added the to database:
// Mutations
@MutationMapping
public Person addPerson(@Argument final AddPerson input) {
return personRepository.save(new Person(null, input.name(), input.age()));
}
Here the @Argument annotation is important to indicate an input type (as explained above) is needed containing all information to add a new Person. Furthermore the client is able to subscribe to a stream of data forwarded by the GraphQL engine. Subscription mappings will use the websocket protocol, therefore a GraphQL websocket path (see above) must be configured. E.g. to test subscriptions with GraphiQL following URL must be used: http://localhost:8080/graphiql?path=/graphql&wsPath=/graphqlws. Then a specifc subscription request can be send to the server and the subscription will be initiated (including the protocol change). Class NumberController implements a method called numbers providing a random number each second:
@Controller
public class NumberController {
private final Random random = new Random();
@SubscriptionMapping
public Flux<Integer> numbers(@Argument final int bound) {
return Flux.interval(Duration.ofSeconds(1)).map(x -> random.nextInt(bound));
}
}
More information can be found at: https://docs.spring.io/spring-graphql/docs/current/reference/html/#overview
Testing
GraphQL APIs can be (integration) tested using @SpringBootTest and @AutoConfigureHttpGraphQlTester annotation. Following class also uses @TestContainers to provide a PostgresDB as backend for proper testing:
@SpringBootTest
@AutoConfigureHttpGraphQlTester
@Testcontainers
class GraphQLIntegrationTest {
@Container
static JdbcDatabaseContainer<?> database = //
new PostgreSQLContainer<>("postgres:13") //
.withDatabaseName("graphql") //
.withUsername("test") //
.withPassword("test") //
.withInitScript("setup.sql");
@DynamicPropertySource
static void setDatasourceProperties(final DynamicPropertyRegistry dynamicPropertyRegistry) {
dynamicPropertyRegistry.add("spring.datasource.url", database::getJdbcUrl);
dynamicPropertyRegistry.add("spring.datasource.username", database::getUsername);
dynamicPropertyRegistry.add("spring.datasource.password", database::getPassword);
}
@Autowired
HttpGraphQlTester httpGraphQlTester;
@Test
void queryAddressesField() {
httpGraphQlTester.document(
"""
query {
addresses {
personId
street
city
}
}
"""
).execute() //
.path("data.addresses[0].personId").entity(Long.class).isEqualTo(1000L) //
.path("data.addresses[0].street").entity(String.class).isEqualTo("Street1") //
.path("data.addresses[0].city").entity(String.class).isEqualTo("Munich") //
.path("data.addresses[1].personId").entity(Long.class).isEqualTo(2000L) //
.path("data.addresses[1].street").entity(String.class).isEqualTo("Street2") //
.path("data.addresses[1].city").entity(String.class).isEqualTo("Berlin");
}
@Test
void mutationAddPerson() {
httpGraphQlTester.document(
"""
mutation {
addPerson(input:{name:"Ralf", age:100}) {
id
name
age
}
}
"""
).execute() //
.path("data.addPerson.id").entity(Long.class).matches(id -> id > 0) //
.path("data.addPerson.name").entity(String.class).isEqualTo("Ralf") //
.path("data.addPerson.age").entity(Integer.class).isEqualTo(100);
}
}
This test basically verifies if addresses which have been added by init script are available when being requested (by query request) as well as if additional persons can be added to the database (by mutation request).
Conclusion
Spring for GraphQL provides a handy framework to design, implement and test GraphQL APIs. It is seemlessly integrated into the Spring (SpringBoot) eco system, e.g. by securing the GraphQL API using Spring Security, instumentation using Micrometer or integrating with Project Reactor to create efficient reactive systems. Furthermore it provides GraalVM Native support and offers an inbuild tool GraphiQL which is very helpful for development and testing of GraphQL APIs.