Introduction to Spring for GraphQL

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

TypeDescription
IDThe ID scalar type represents a unique identifier. It it serialized same way as a String
StringA UTF-8 character sequence
IntA signed 32-bit integer
FloatA signed double-precision floating point value
Booleantrue or false
Scalar Types

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:

1. Client / API Consumer sends GraphQL request to API Service using defined transportation channel.
2 / 3. GraphQL engine parses request and calls handler methods to fetch data from one or multiple sources.
4. GraphQL engine prepares response and returns a JSON with requested data.

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:

GraphiQl. If enabled per default reachable at http://localhost:8080/graphiql?path=/graphql

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.

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Nach oben scrollen
Cookie Consent Banner von Real Cookie Banner