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.