Part 1: GraphQL with Spring Boot, JPA and Kotlin

GraphQL is a query language for dynamic queries on linked data. In the GraphQL world, there are many tutorials and documentation that illustrate the concrete introduction to GraphQL. Since getting started requires a lot of explanation, regular tutorials usually have a simple setup, which does not reflect the real world complexity. If additional technologies are added or the complexity of the project increases, these “Hello World” examples are usually no longer so helpful.

This article introduces GraphQL on the technology Spring Boot with JPA and Kotlin. I also present some best practices which I have learned while I have worked with it. You can find the source code here: https://github.com/akquinet/kotlin-spring-graphql

I will not explain the basics of GraphQL and the other technologies used here. Therefore, it is advisable be familiar with the following technologies:

General Architecture

A Spring Boot application usually consists of 4 layers, with each layer only interacting directly with its adjacent one.

Simplified Spring Boot layers. Image src

In this article, we will focus on the presentation layer. The application can be reached externally via this layer. Usually via a REST endpoint. GraphQL uses exactly one of these endpoints to receive a GraphQL query or mutation by POST method. The Persistence Layer uses JPA to communicate with the Database.

Basic Setup

To set up the application-skeleton (without GraphQL), the Spring Boot Starter Project was used:

  • Kotlin (1.3.10)
  • Spring-Boot-Starter (with JPA, Web) (2.1.6)
  • Database MariaDB (2.4.2)

Demo application context

To demonstrate the GraphQL integration, the application has created a relationship between people and their pets in the persistence layer. Each person has exactly one address. Each person has multiple pets. See Entity.

@Entity
class PersonEntity(
        var name: String,
        @OneToOne(cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY)
        val address: AddressEntity,
        @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY, mappedBy = "owner")
        val pets: List<PetEntity> = ArrayList(),
        @Id @GeneratedValue
        var id: Long? = null
)

@Entity
class AddressEntity(
        var street: String,
        var zipCode: String,
        var city: String,
        @Id @GeneratedValue
        var id: Long? = null
)

@Entity
class PetEntity(
        @JsonIgnore
        @ManyToOne(fetch = FetchType.LAZY)
        var owner: PersonEntity,
        var name: String,
        @Enumerated(EnumType.STRING)
        var type: PetType,
        @Id @GeneratedValue
        var id: Long? = null
)

enum class PetType { CAT, DOG, BIRD }

Integrate GraphQL

To integrate GraphQL into a Spring Boot application, the following steps are necessary:

  1. Add GraphQL dependencies
  2. Configure Spring application
  3. Create GraphQL schema
  4. Create a GraphQL resolver

1. Add GraphQL dependencies

Similar to the Spring-Boot-starter project there is also a Graphql-Spring-Boot-starter project, in which all necessary / common dependencies from Spring Boot and GraphQL are included (see dependencies). The GraphQL-Spring-Boot-Starter project creates a Spring Boot application with spring-boot-starter-websocket, spring-boot-starter-web and spring-boot-starter-actuator. Of course it also contains graphql-java-servlet dependencies. The configuration magic of GraphQL is done by graphql-java-tools and thus ensures that minimal boilerplate code as possible is needed. This also includes using the schema-first pattern. To do this, the application loads the GraphQL schema at startup and provide the associated GraphQL endpoint. The alternative would be the hard-to-read schema-builder in the code. Afterwards all classes are searched for data resolvers, validated them against the already loaded schema, and make them available as beans.

To use this, the following dependencies must be added in build.gradle.kts:

dependencies {
    compile("com.graphql-java-kickstart:graphql-spring-boot-starter:5.10.0")
    compile("com.graphql-java-kickstart:altair-spring-boot-starter:5.10.0")
}

Be sure to use Kotlin Version >= 1.3.10, see doc.

Similar to Swagger GraphQL has an UI available to see the interface definition. This UI can be used to view the schema definition as well as sending requests to the server. I opted for the alternative UI altair instead of using the regular GraphiQL UI, as altair offers more functionality. For example, altair can also set headers for authorization, which is not possible with GraphiQL.

Here an example of the Altair-UI:

Alternative GraphQL UI for querying data

2. Configure Spring application

Next, the application must be configured for GraphQL. This requires the following configuration in the application.yml:

graphql:
  servlet:
    mapping: /graphql
    enabled: true
    corsEnabled: false
    exception-handlers-enabled: true
  tools:
    schema-location-pattern: "**/*.graphqls"
    # Enable or disable the introspection query. Disabling it puts your server in contravention of the GraphQL
    # specification and expectations of most clients, so use this option with caution
    introspection-enabled: true


altair:
  enabled: true
  mapping: /altair
  endpoint.graphql: /graphql
  pageTitle: Altair
  props.resources.variables: graphqls/altair/variables.graphql

GraphQL explanations:

  • mapping: /graphql defines the REST-endpoint used to execute the GraphQL queries.
  • schema-location-pattern: " **/*.Graphqls" sets the path to the schema files.

Altair explanations:

  • mapping: /altair defines the path to the GraphQL altair-UI.
  • endpoint.graphql: /graphql path where the application hosts the GraphQL-endpoint
  • props.resources.variables: graphqls/altair/variables.graphql defines the path to a file with predefined variables for the queries in the altair-UI.

If the variables file does not exist, the default value in the Altair UI is an invalid value for the variables, so it must first be changed before a request can be sent. Therefore, it is recommended to set a file with default variables, which can be empty.

3. Create GraphQL schema

After the Spring Boot application has been configured for GraphQL, the next step is creating the GraphQL schema. It must be located in a file under the graphql.tools.schema-location-pattern path defined in the previous step.

The following schema describes the demo application context with persons and their address and pets:

type Query {
    persons: [Person!]!
    person(id: ID!): Person
}
type Person {
    id: ID!
    name: String
    address: Address
    pets: [Pet!]!
}
type Address {
    id: ID!
    street: String
    zipCode: String
    city: String
}
enum PetType {
    CAT
    DOG
    BIRD
}
type Pet {
    id: ID!
    owner: Person!
    name: String!
    reverseName: String
    type: PetType!
}

Here is quick overview in the GraphQL schema syntax:

  • Possible GraphQL-operations are defined with these types: Query or Mutation. All other defined types like Person or Pet only to structure data. The Query-Operation fetches data and Mutation-Operation writes data. The fields in those operation-types represents all possible GraphQL-entrypoints. It is mandatory to implement at least one type of them, otherwise you can’t interact with GraphQL. In the example the persons field is used for querying all persons.
  • GraphQL supports the following base types: StringIntFloatBoolean and ID. Types are written after a field and separated with a “:“. Every other type must be defined with the type keyword. E.g. the custom types Person or Pet.
  • Fields with braces represents data with related params (e.g. only a Person with a given ID).
  • [...]defines a list of objects with the given type.
  • ! ensures the value is not null.

With this GraphQL-schema we can execute the following query, which uses the Query-Operation and ask for all persons:

query {
  persons {
    name
  }
}

Response:

{
  "data": {
    "persons": [
      {
        "name": "Alice"
      },
      {
        "name": "other name"
      }
    ]
  }
}

4. Create a GraphQL resolver

Now let’s take a look, how to implement the schema definitions in the Spring application. For this you have to implement different resolver types:

  • The root type Query needs an implementation of GraphQLQueryResolver
  • The root type Mutation needs an implementation of GraphQLMutationResolver
  • Each self.defined type has to implement the GraphQLResolver

The root type implementations GraphQLQueryResolver or GraphQLMutationResolver must implement all methods with the field-names the schema defines. Here is an example implementation of the GraphQLQueryResolver for the GraphQL-schema in the previous step:

@Component
class PersonQueryResolver @Autowired constructor(private val personRepository: PersonRepository) : GraphQLQueryResolver {

    fun persons(): List<PersonEntity> = personRepository.findAll()

    fun person(id: Long): PersonEntity = personRepository
            .findById(id)
            .orElseThrow { Exception("Can not find person with id: $id") }
}

For the other custom types you need to implement GraphQLResolver. With the GraphQLResolver it is not necessary to implement all fields. If a method is missing the default PropertyDataFetcher fetches the field with the same name from the implemented resolver entity-type. For example for type Person:

// simplified person entity without persistence annotations
class PersonEntity(
        var name: String,
        val address: AddressEntity,
        val pets: List<PetEntity> = ArrayList(),
        var id: Long? = null
)

@Component
class PersonResolver : GraphQLResolver<PersonEntity> {
    // not necessary implementation for the person-schema-field name
    fun name(person: PersonEntity): String = person.name
}

In this example, this GraphQLResolver does not need to implement any resolver method (like name), because of the same field-names between the schema-type Person and the PersonEntity. Of course it is possible to override the default DataFetcher.

Let’s take a look at an modified schema with the field reverseName for a Person:

type Person {
    ...
    reverseName: String!
}

Now you must implement a reverseName method in the resolver, because the PersonEntity does not have a field named like this:

@Component
class PersonResolver : GraphQLResolver<PersonEntity> {
    fun reverseName(person: PersonEntity): String {
        return person.name.reversed()
    }
}

Finally, the Spring Boot application can be started and requests made with the Altair UI (http://localhost:8080/altair).

Get the person with ID 1

CONCLUSION

Compared to a REST interface, GraphQL offers many advantages. Especially for the client, as this gives it the freedom to send dynamic queries to the server. But in the backend much more development effort for the GraphQL interface has to be included. GraphQL is more complex in the backend than just a simple REST interface.

GraphQL is especially worthwhile for applications with many entities with many relations between each another. In doing so, it would be more and more time-consuming to implementing all necessary REST endpoints. With REST it is also often necessary to query several REST endpoints for a query.

By using the Graphql-Spring-Boot-starter project, the project setup has been made very easy to integrate GraphQL into Spring.

Thus, depending on the application area, it should be decided whether the extra effort by GraphQL is worthwhile.

The proof of concept of GraphQL with Spring Boot, JPA and Kotlin is verified by this article.

The next steps should be to take a deeper look how to work with GraphQL:

  • How to write GraphQL-Tests
  • How to handle GraphQL-Errors
  • Are there some best practices?

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.