fbpx

GraphQL SPQR with Spring Boot and Kotlin

1. Introduction

In my previous article, I’ve shown you how to create a simple GraphQL Spring Boot project using GraphQL schema files. This time, I would like to teach you another approach- the GraphQL SPQR (GraphQL Schema Publisher & Query Resolver).

To put it simply, GraphQL SPQR dynamically generates a schema from the source code. With this approach, we don’t need to define *.graphqls files anymore, but it will require us to add some additional configuration to the project.

2. Imports

Just like in the previous article, we will start with the imports. However, this time the graphql-java-tools dependency will be replaced by the SPQR:

implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.graphql-java:graphql-spring-boot-starter:5.0.2")
implementation("com.graphql-java:graphiql-spring-boot-starter:5.0.2")
implementation("io.leangen.graphql:spqr:0.10.1")

3. Create Models

As the next step, let’s define our POJOs.

Let’s start by adding a Company class with 4 simple properties:

class Company(
    val id: Long,
    val name: String,
    val address: String,
    val zipCode: String
)

Nextly, let’s implement the Employee class just like below:

class Employee(
    val id: Long,
    val firstName: String,
    val lastName: String,
    val status: EmployeeStatus,
    val company: Company
)

enum class EmployeeStatus {
    ACTIVE, RETIRED
}

4. Create Repositories

As the third step, we need to implement the logic responsible for data management. To keep this article as simple as possible, we won’t be connecting to any external data source (anyway, this architecture might be a great entry point for further development).

4.1. Implement CompanyRepository

Firstly, let’s create the CompanyRepository class and annotate it with @Component:

@Component
class CompanyRepository {

    private val companyId = AtomicLong(4)

    private val companies = mutableSetOf(
        Company(1, "Company One", "Address 1", "10001"),
        Company(2, "Company Two", "Address 2", "10002"),
        Company(3, "Company Three", "Address 3", "10003"),
        Company(4, "Company Four", "Address 4", "10004")
    )
}

CompanyRepository contains two properties: companyId– an atomic variable, which will be used to generate identifiers for companies; and companies– a set containing predefined objects.

Nextly, let’s add functions responsible for searching, creating, and deleting companies:

fun findAll(): Set = companies

fun findById(companyId: Long): Company =
    companies.find { it.id == companyId }
        ?: throw RuntimeException("Company with id [$companyId] could not be found.")

fun create(name: String, address: String, zipCode: String): Company {
    val company = Company(
        id = companyId.incrementAndGet(),
        name = name,
        address = address,
        zipCode = zipCode
    )

    companies.add(company)
    return company
}

fun delete(id: Long): Boolean =
    companies.removeIf { it.id == id }

4.2. Implement EmployeeRepository

With that being done, we can create the EmployeeRepository as follows:

@Component
class EmployeeRepository(
    private val companyRepository: CompanyRepository
) {

    val employerId = AtomicLong(3)

    val employees = mutableSetOf(
        Employee(1, "John", "Doe", EmployeeStatus.ACTIVE, companyRepository.findById(1)),
        Employee(2, "Adam", "Nowak", EmployeeStatus.ACTIVE, companyRepository.findById(2)),
        Employee(3, "Stan", "Bar", EmployeeStatus.RETIRED, companyRepository.findById(3))
    )
}

Similarly, the EmployeeRepository will contain 4 simple functions:

fun findAll(): Set = employees

fun findById(employeeId: Long) =
    employees.find { it.id == employeeId }
        ?: throw RuntimeException("Employee with id [$employeeId] could not be found.")

fun create(
    firstName: String,
    lastName: String,
    status: EmployeeStatus,
    companyId: Long
): Employee {
    val company = companyRepository.findById(companyId)

    val employee = Employee(
        id = employerId.incrementAndGet(),
        firstName = firstName,
        lastName = lastName,
        status = status,
        company = company
    )

    employees.add(employee)
    return employee
}

fun delete(id: Long): Boolean =
    employees.removeIf { it.id == id }

5. Define Queries

In simple terms, Queries are responsible for retrieving data from our server. You might consider it something, like GET requests in REST APIs.

Let’s start by creating the CompanyQuery class as follows:

@Component
class CompanyQuery(
    private val companyRepository: CompanyRepository
) {

    @GraphQLQuery(name = "companies")
    fun companies(): Set<Company> =
        companyRepository.findAll()

    @GraphQLQuery(name = "companyById")
    fun companyById(id: Long): Company =
        companyRepository.findById(id)
}

As you can see, it’s just a simple Spring Boot component. However, if we want to expose our functions, we need to annotate them with @GraphQLQuery. Moreover, query and function names do not need to match at all- we can name them independently.

Similarly, let’s create the EmployeeQuery class:

@Component
class EmployeeQuery(
    private val employeeRepository: EmployeeRepository
) {

    @GraphQLQuery(name = "employees")
    fun employees(): Set<Employee> =
        employeeRepository.findAll()

    @GraphQLQuery(name = "employeeById")
    fun employeeById(id: Long): Employee =
        employeeRepository.findById(id)
}

6. Create Mutations

Our next step will be the implementation of Mutations. They work in a similar way, but we use them to modify the data (you might consider them as the equivalent of POST/PUT/PATCH/DELETE handlers).

Let’s start with creating the CompanyMutation class:

@Component
class CompanyMutation(
    private val companyRepository: CompanyRepository
) {

    @GraphQLMutation(name = "newCompany")
    fun newCompany(name: String, address: String, zipCode: String): Company =
        companyRepository.create(name, address, zipCode)

    @GraphQLMutation(name = "deleteCompany")
    fun deleteCompany(id: Long): Boolean =
        companyRepository.delete(id)
}

Please notice, that this time we’ve used the @GraphQLMutation annotation instead.

Finally, let’s implement the EmployeeMutation in a similar manner:

@Component
class EmployeeMutation(
    private val employeeRepository: EmployeeRepository
) {

    @GraphQLMutation(name = "newEmployee")
    fun newEmployee(
        firstName: String,
        lastName: String,
        status: EmployeeStatus,
        companyId: Long
    ): Employee =
        employeeRepository.create(firstName, lastName, status, companyId)

    @GraphQLMutation(name = "deleteEmployee")
    fun deleteEmployee(id: Long): Boolean =
        employeeRepository.delete(id)
}

7. Prepare Configuration File

After all the above is finished, we need to implement a schema generator and configure the GraphQL bean.

Let’s start by adding the GraphQLConfig class with the prepareGraphQLSchema function:

@Configuration
class GraphQLConfig {
    private fun prepareGraphQLSchema(
        companyQuery: CompanyQuery, employeeQuery: EmployeeQuery,
        companyMutation: CompanyMutation, employeeMutation: EmployeeMutation
    ): GraphQLSchema =
        GraphQLSchemaGenerator()
            .withResolverBuilders(
                AnnotatedResolverBuilder(),
                PublicResolverBuilder("com.codersee.graphqlspqr")
            )
            .withOperationsFromSingletons(companyQuery, employeeQuery, companyMutation, employeeMutation)
            .withValueMapperFactory(JacksonValueMapperFactory())
            .generate()
}

Please notice, that the String passed to the PublicResolverBuilder constructor has to reflect the package structure of your project.

Lastly, let’s set up our GraphQL bean using the already implemented schema:

@Bean
fun graphQL(
    companyQuery: CompanyQuery, employeeQuery: EmployeeQuery,
    companyMutation: CompanyMutation, employeeMutation: EmployeeMutation
): GraphQL {
    val schema = prepareGraphQLSchema(companyQuery, employeeQuery, companyMutation, employeeMutation)

    return GraphQL.newGraphQL(schema)
        .queryExecutionStrategy(AsyncExecutionStrategy())
        .instrumentation(
            ChainedInstrumentation(
                listOf(
                    MaxQueryComplexityInstrumentation(100),
                    MaxQueryDepthInstrumentation(10)
                )
            )
        )
        .build()
}

As you can see, besides the execution strategy, we’ve configured the maximum complexity and depth values for our queries and mutations.

8. Implement the Controller

As the last step, we need to create the entry point for our queries.

Let’s create a class called GraphQLController. Additionally, let’s inject the GraphQL bean configured in the previous step:

@RestController
class GraphQLController(
    private val graphQL: GraphQL
) {

    @PostMapping("/graphql")
    @ResponseBody
    fun execute(@RequestBody request: Map<String, String>): ExecutionResult {
        return graphQL
            .execute(
                request["query"].toString()
            )
    }
}

As you can see, the only function we need to implement here is the execute, which will be responsible for handling all the GraphQL requests.

9. Testing

Finally, we can run the application and test it. Just like in the previous article, we will use the GraphiQL– a dedicated GUI for communicating with GraphQL servers.

In the beginning, we’ve added the GraphiQL Spring Boot Starter dependency, which allows us to run its web-based version under the /graphiql endpoint. However, you can always download it’s Electron-based wrapper here.

9.1. Test Queries

After starting the application, let’s head to the http://localhost:8080/graphiql endpoint and run the following query:

query {
  employees {
    id
    firstName
    lastName
    status
    company {
      name
    }
  }
}

As you can see, the query structure allows us to define, which fields we would like to fetch. I highly suggest you check out different combinations and see what will be the results. As a result of the above query, we should see the following output:

{
  "data": {
    "employees": [
      {
        "id": "1",
        "firstName": "John",
        "lastName": "Doe",
        "status": "ACTIVE",
        "company": {
          "name": "Company One"
        }
      }
...

Nextly, let’s try to find the employee by id:

query {
  employeeById(id: 1) {
    id
    firstName
    lastName
    status
    company {
      id
      name
    }
  }
}

# Result:

{
  "data": {
    "employeeById": {
      "id": "1",
      "firstName": "John",
      "lastName": "Doe",
      "status": "ACTIVE",
      "company": {
        "id": "1",
        "name": "Company One"
      }
    }
  }
}

Similarly, we can test company queries:

query {
  companies {
    id
    name
    address
    zipCode
  }
}

# Result:

{
  "data": {
    "companies": [
      {
        "id": "1",
        "name": "Company One",
        "address": "Address 1",
        "zipCode": "10001"
      },
...
query {
  companyById(id: 1) {
    id
    name
    address
    zipCode
  }
}

# Result:

{
  "data": {
    "companyById": {
      "id": "1",
      "name": "Company One",
      "address": "Address 1",
      "zipCode": "10001"
    }
  }
}

9.1. Test Mutations

Testing mutations is quite similar to queries. The only difference is the usage of a mutation keyword:

mutation {
  newCompany(name: "New", address: "Address new", zipCode: "10201") {
    id
    name
    address
    zipCode
  }
}

This time, a new company will be created, and the following data will be returned:

{
  "data": {
    "newCompany": {
      "id": "5",
      "name": "New",
      "address": "Address new",
      "zipCode": "10201"
    }
  }
}

Similarly, we can create a new employee:

mutation {
  newEmployee(firstName: "Piotr", lastName:"Wolak", status: ACTIVE, companyId: 2) {
    id
    firstName
    lastName
    status
    company {
      id
      name
    }
  }
}

# Result:

{
  "data": {
    "newEmployee": {
      "id": "4",
      "firstName": "Piotr",
      "lastName": "Wolak",
      "status": "ACTIVE",
      "company": {
        "id": "2",
        "name": "Company Two"
      }
    }
  }
}

Finally, let’s test delete mutations:

{
  "data": {
    "deleteCompany": true
  }
}

# Result:

{
  "data": {
    "deleteCompany": true
  }
}
mutation {
  deleteEmployee(id: 1)
}

# Result:

{
  "data": {
    "deleteEmployee": true
  }
}

9. Conclusion

And that would be all for this tutorial. I really hope that this article helped you to get a better understanding of how to create a GraphQL SPQR project with Spring Boot and Kotlin.

As always, you can find the working source code in our GitHub repository.

If you would like to ask about anything or share any feedback with me, I would be more than happy to hear from you. You can always contact me via fan page, group, or a contact form.

Share on facebook
Share on twitter
Share on linkedin

Leave a Comment

Your email address will not be published. Required fields are marked *

Subscribe to our Newsletter

Join the community and get free eBooks.

Image shows the covers of free ebooks accessible for newsletter subscribers.

You may opt out any time. Terms of Use and Privacy Policy

Find us also on...

Join the FREE weekly newsletter and get two free eBooks as well:

Image shows the covers of free ebooks accessible for newsletter subscribers.

You may opt out any time. Terms of Use and Privacy Policy

To make Codersee work, we log user data. By using our site, you agree to our Privacy Policy and Terms of Use.