Skip to main content

Recommended Usage

In general, the resolverInterfaces config should be used to generate more performant code. This is especially important when dealing with expensive operations, such as database queries or network requests. When at least one field has arguments in a type, we generate an open class with function signatures to be inherited in source code. However, when fields have no arguments, we generate data classes by default.

Example

The following example demonstrates the problem with using generated data classes to implement your resolvers with GraphQL Kotlin.

Say you want to implement the schema below:

type Query {
resolveMyType(input: String!): MyType
}

type MyType {
foo: String!
bar: String
}

Here is the default behavior.

Generated Kotlin:

package com.types.generated

open class Query {
open fun resolveMyType(input: String): MyType = throw NotImplementedError("Query.resolveMyType must be implemented.")
}

data class MyType(
val foo: String,
val bar: String? = null
)

Source code:

import com.expediagroup.graphql.server.operations.Query
import com.types.generated.MyType
import com.types.generated.Query as QueryInterface

class MyQuery : Query, QueryInterface() {
override fun resolveMyType(input: String): MyType =
MyType(
foo = myExpensiveCall1(),
bar = myExpensiveCall2()
)
}

The resulting source code is extremely unperformant. The MyType class is a data class, which means that the foo and bar properties are both initialized when the MyType object is created, and myExpensiveCall1() and myExpensiveCall2() will both be called in sequence! Even if I only query for foo, not only will myExpensiveCall2() still run, but it will also wait until myExpensiveCall1() is totally finished.

Instead, use the resolverInterfaces config!

Codegen config:

import { GraphQLKotlinCodegenConfig } from "@expediagroup/graphql-kotlin-codegen";

export default {
resolverInterfaces: [
{
typeName: "MyType",
},
],
} satisfies GraphQLKotlinCodegenConfig;

Generated Kotlin:

package com.types.generated

open class Query {
open fun resolveMyType(input: String): MyType = throw NotImplementedError("Query.resolveMyType must be implemented.")
}

open class MyType {
open fun foo(): String = throw NotImplementedError("MyType.foo must be implemented.")
open fun bar(): String? = throw NotImplementedError("MyType.bar must be implemented.")
}

Source code:

import com.expediagroup.graphql.server.operations.Query
import com.types.generated.Query as QueryInterface
import com.types.generated.MyType as MyTypeInterface

class MyQuery : Query, QueryInterface() {
override fun resolveMyType(input: String): MyType = MyType()
}

class MyType : MyTypeInterface() {
override fun foo(): String = myExpensiveCall1()
override fun bar(): String? = myExpensiveCall2()
}

This code is much more performant! The MyType class is no longer a data class, so the foo and bar properties can now be resolved independently of each other. If I query for only foo, only myExpensiveCall1() will be called, and if I query for only bar, only myExpensiveCall2() will be called.

Check out the related GraphQL Kotlin docs for more information on this topic.