1. Introduction
In this step by step tutorial, I would like to show you how to use GraphQL with Spring Boot and Kotlin. Additionally, we will learn how to test with GraphiQL– a dedicated GUI for editing and testing GraphQL queries and mutations.
In simple words, GraphQL is a query language for APIs that provides a more flexible alternative to REST. It allows clients to define the structure of the desired data, and the same structure of the data is returned from the server, therefore preventing large amounts of data from being returned. It was developed internally by Facebook in 2012 and open-sourced in 2015.
2. Imports
As the first step, let’s add the necessary imports to our project:
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("com.graphql-java:graphql-java-tools:5.2.4")
As you can see, besides Spring Boot starters, we’ve added the GraphQL Java Tools library. It allows us to use the GraphQL schema language to build our graphql-java schema. Additionally, it parses the given GraphQL schema and allows us to BYOO (bring your own object) to fill in the implementations.
3. Create Models
As the next step, let’s implement our models. In this tutorial, we will implement a simple CRUD service for employees’ management.
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 )
As the next step, let’s implement the Employee class:
class Employee( val id: Long, val firstName: String, val lastName: String, val status: EmployeeStatus, val company: Company ) enum class EmployeeStatus { ACTIVE, RETIRED }
Besides the class, we’ve also added the EmployeeStatus enum, which will be used as one of the properties of Employee. Additionally, each employee will have a company property indicating the associated company.
4. Create Repositories
Nextly, let’s create repositories responsible for in-memory data management.
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.
As the next step, 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 have 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 GraphQL Schema
After we’ve implemented our models and repositories, we can start defining the schema. In simple terms, a schema is a contract between the client and the server describing the structure of the data and how the data might be operated.
For the purpose of this tutorial, let’s focus on the 4 types, we will be using:
- Object Types– the most basic components, which represent a kind of objects we can fetch from our service (classes in our example)
- Enums– a special kind of scalar values restricted to a particular set of values
- Queries– every GraphQL service needs to have a Query. To put it simply, queries are used to describe how to read, or fetch values
- Mutations– work in a similar way, as queries and are used to modify a server-side data
If you would like to learn more about the GraphQL Schema, please refer to the official documentation.
5.1. Describe Company Schema
Let’s start with the definition of company schema. Let’s create a company.graphqls file and put it under the ./src/main/resources/ directory:
type Company { id: ID! name: String! address: String! zipCode: String! } type Query { companies: [Company] companyById(id: ID!) : Company } type Mutation { newCompany(name: String!, address: String!, zipCode: String!) : Company! deleteCompany(id: ID!) : Boolean }
In this simple file, we’ve declared three types:
- The Company type– which will be associated with the Company entity, we’ve created earlier
- The Query type– containing two queries
- The Mutation type– describing mutations for company creation and removal
5.2. Describe Employee Schema
As the next step, let’s create an employee.graphqls file:
type Employee { id: ID! firstName: String! lastName: String! status: EmployeeStatus! company: Company! } enum EmployeeStatus{ ACTIVE, RETIRED } extend type Query { employees: [Employee] employeeById(id: ID!) : Employee } extend type Mutation { newEmployee(firstName: String!, lastName: String!, status: EmployeeStatus!, companyId: ID!) : Employee! deleteEmployee(id: ID!) : Boolean }
This file is pretty similar to the previous one, but we’ve also added the Enum type here. Additionally, please notice the extend keyword- we have to place it in order to extend the Mutation and Query declared in another file.
6. Create Queries Resolvers
After we’ve defined the schema, we can start adding the code responsible for data manipulation. Let’s start by adding the CompanyQuery class:
@Component class CompanyQuery( private val companyRepository: CompanyRepository ) : GraphQLQueryResolver { fun companies(): Set = companyRepository.findAll() fun companyById(id: Long): Company = companyRepository.findById(id) }
Please notice two important things here. Firstly, the CompanyQuery class implements the GraphQLQueryResolver, which is a markup interface provided by the GraphQL dependency. Secondly, public functions need to match the schema in order to be resolved correctly.
Similarly, let’s implement the EmployeeQuery class:
@Component class EmployeeQuery( private val employeeRepository: EmployeeRepository ) : GraphQLQueryResolver { fun employees(): Set = employeeRepository.findAll() fun employeeById(id: Long): Employee = employeeRepository.findById(id) }
7. Implement Mutations Resolvers
In order for our application to be fully functional, we need to add the resolvers for our mutations. As the first step, let’s add the CompanyMutation class:
@Component class CompanyMutation( private val companyRepository: CompanyRepository ) : GraphQLMutationResolver { fun newCompany(name: String, address: String, zipCode: String): Company = companyRepository.create(name, address, zipCode) fun deleteCompany(id: Long): Boolean = companyRepository.delete(id) }
Just like in Queries resolvers, public functions need to match the structure from the defined schema. The only difference here is that we’ve implemented the GraphQLMutationResolver interface.
Finally, we can add the last class to our project which will be responsible for the employee mutations:
@Component class EmployeeMutation( private val employeeRepository: EmployeeRepository ) : GraphQLMutationResolver { fun newEmployee( firstName: String, lastName: String, status: EmployeeStatus, companyId: Long ): Employee = employeeRepository.create(firstName, lastName, status, companyId) fun deleteEmployee(id: Long): Boolean = employeeRepository.delete(id) }
8. Testing
Finally, we can run the application and test it. In this article, I would like to show you the tool called 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.
8.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" } } }
8.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 you have learned how to use GraphQL with Spring Boot and Kotlin and that testing with GraphiQL will be a piece of cake for you.
For the source code, please refer to this 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.