GraphQL’s interfaces and unions provide a good way to handle multiple field types in queries
Illustration of GraphQL with Spring Boot
First and Foremost
I’ll start with the explanation of the concepts, and then we’ll look into its implementation with Spring Boot and Kotlin.
Consider the following data classes and interface.
interface Profile {
val id: UUID,
val name: String
}
data class User {
override val id: UUID,
override val name: String
}: Profile
data class Company {
override val id: UUID,
override val name: String,
val address: String
}: Profile
Now we’re going to write a GraphQL schema that models the same interfaces and classes. We’ll have a query to fetch the Profile
, which will give us aUser
or Company
based on what the client needs in a single query.
In our GraphQL schema, when using interfaces, we’ll add the following.
interface Profile {
id: ID
name: String
}
type User implements Profile {
id: ID
name: String
}
type Company implements Profile {
id: ID
name: String
address: String
}
type Query {
search(text: String!): [Profile]!
}
We can also use Union
instead of Interface
.
type User {
id: ID
name: String
}
type Company {
id: ID
name: String
address: String
}
union Profile = User | Company
type Query {
search(text: String!): [Profile]!
}
Which one should we use?
It can vary based on the use case and your end goal. The following points can be used to finalize the option.
There isn’t always an advantage to grouping shared fields into interfaces — it depends on the use case
Interfaces are good for when the types have a fundamental commonality in how they should be used
Unions are good for documenting and forcing the client to understand how different types should be treated
Implementation With Spring Boot
1. The setup
You can skip to the third section if you know how to set up a Spring Boot project with GraphQL. Also, the code to this is available here.
Otherwise, go to https://start.spring.io/.
Select a Gradle
project, and set the language as Kotlin
.
You don’t have to select any dependencies for a bare-minimum implementation.
Download the ZIP file, and open it in your favorite IDE.
Jump to build.gradle.kts
, and add:
implementation("com.graphql-java-kickstart:graphql-spring-boot-starter:6.0.1")
implementation("com.graphql-java-kickstart:graphql-java-tools:5.7.1")
runtimeOnly("com.graphql-java-kickstart:altair-spring-boot-starter:6.0.1")
runtimeOnly("com.graphql-java-kickstart:graphiql-spring-boot-starter:6.0.1")
runtimeOnly("com.graphql-java-kickstart:voyager-spring-boot-starter:6.0.1")
runtimeOnly("com.graphql-java-kickstart:playground-spring-boot-starter:6.0.1")
These are the GraphQL libraries you’d need to get started with your first endpoint.
2. The first endpoint
Create a file called schema.graphqls
inside the resources
directory.
Add the following lines:
type Query{
hello: String
}
And then create a resolver
class that can actually do something when the query is triggered by the client.
@Component
class HelloWorldResolver: GraphQLQueryResolver {
fun hello(): String {
return "Hello World!"
}
}
That’s it.
Run the server using ./gradlew bootRun
.
Go to your browser, and open http://localhost:8080/playground
. The Playground is one of the developer tools used as a client for GraphQL APIs.
This will be the output when you trigger the query.
Query and response in the Playground
That’s it.
3. The interface implementation
Let’s start by changing the schema. In your schema.graphqls
file, add the following code.
type Query{
hello: String
getIVehicles: [IVehicle]!
}
interface IVehicle{
name: String
price: Int
}
type ICar implements IVehicle {
name: String
price: Int
engineType: String
}
type IBike implements IVehicle {
name: String
price: Int
}
Now add some Kotlin classes and interfaces to match the schema entities.
interface IVehicle {
val name: String
}
data class IBike(override val name: String, val price: Int): IVehicle
data class ICar(override val name: String, val price: Int, val engineType: String) : IVehicle
After this mapping, we can start creating the service that’ll fetch the IVehicle
data when the getIVehicles
query is triggered.
You need to create Resolver
and a Service
.
@Component
class VehicleResolver(private val vehicleService: VehicleService) : GraphQLQueryResolver {
fun getIVehicles(): List<IVehicle> {
return vehicleService.getIVehicles()
}
}
@Service
class VehicleService {
fun getIVehicles(): List<IVehicle> {
return listOf(
ICar("BMW X1", 45000, "PETROL"),
IBike("Ducati", 90000)
)
}
}
That’s it.
You’ve successfully created an endpoint that can fetch both car and bike data with the same query. How cool is that?
You can run this query to fetch the data.
Query and response on Playground
Wait a minute! I forgot something.
You need to do one more thing before this actually works.
You need to add those interface types in the GraphQL’s schema parser dictionary.
The way to do that is to add this code.
@Configuration
class GraphQLConfig() {
@Bean
fun schemaParserDictionary(): SchemaParserDictionary {
return SchemaParserDictionary()
.add(IBike::class)
.add(ICar::class)
}
}
This needs to be done because there’s a problem in the library, and autodiscovery for these types doesn’t work unless you explicitly put it.
You can find the issue here.
That’s it.
Conclusion
I’m not adding the implementation for Union
here, as it’s pretty similar to the implementation of interfaces. Don’t worry: If you’re interested, you can find the implementation here.
I’m glad you made it to the end. It means you’re interested in implementing GraphQL. Here is another article in which I share my experience of using GraphQL for six months.