Spring for GraphQL with Kotlin Coroutines

At the end of this article, you will know precisely how to create a Spring Boot GraphQL application that exposes queries and mutations with the help of Kotlin suspended functions and Flows.

Long story short, we will see:

how to prepare a GraphQL schema,

what imports are necessary to utilize suspended functions,

how to expose queries, mutations, and map schema with annotated controllers,

exception handling with GraphQlExceptionHandler

Create Project

As the first step, let’s prepare a brand new project.

To do so, let’s navigate to the Spring Initializr page and select the following imports:

The project metadata are totally up to you, but when it comes to the dependencies, we must select the Spring Reactive Web and Spring for GraphQL.

Thanks to the first one, Spring Boot configures a reactive web stack based on Project Reactor and Netty instead of Tomcat. And given we want to work with GraphQL and Kotlin coroutines, then this is a must-have for us.

The second provides support for Spring applications built on GraphQL Java. Moreover, it is the successor of the GraphQL Java Spring project from the GraphQL Java team.

Lastly, let’s download the zip package, extract and import it to our IDE.

Define GraphQL Schema

What is GraphQL Schema?

If you have ever worked with GraphQL before, then you know that compared to the REST, everything starts with schema definition.

If you haven’t, then GraphQL schema is a file typically written with Schema Definition Language (SDL) that describes how our API clients can interact with our server. To be more specific, inside it, we define the structure of returned data, as well as how consumers can fetch or send us the payloads.

And even though it may sound like a GraphQL substitute for OpenAPI specs, it is something more. In REST, OpenAPI docs are a nice addition to our project. In GraphQL, our services continuously utilize the schema definition to validate and execute requests against it.

And if you would like to learn more about GraphQL schema, then check out the official documentation later. But for now, let’s focus on the Spring / GraphQL / Kotlin combination

schema.graphql in Spring

As the first step, let’s head to the resources > graphql directory in our project, and let’s insert the schema.graphql file:

type Query {
article(id: ID!): Article
articles: [Article!]!
}

type Mutation {
createArticle(input: CreateArticleInput!): Article!
addComment(articleId: ID!, input: AddCommentInput!): Comment
}

input CreateArticleInput {
title: String!
content: String!
userId: ID!
}

input AddCommentInput {
userId: String!
content: String!
}

type User {
id: ID!
name: String!
}

type Article {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
createdAt: String!
}

type Comment {
id: ID!
content: String!
author: User
}

In Spring Boot, this file is registered automatically as our schema definition.

And as we can see, we defined 5 things:

queries– how a client reads data. Every GraphQL schema must support them,

mutations– how a client modifies data,

inputs– used to pass structured arguments to our mutations and queries,

types– establishing the structure of data we return,

scalars– like String, or ID (but also Int, Float, Boolean), describing returned fields.

Moreover, we can see the exclamation mark- !. In contrast to Kotlin, in GraphQL, we must explicitly define non-nullable types. Meaning, that [Comment] describes a nullable array of nullable Comment type (Array<Comment?>?), whereas [Comment!]! is a not null array with not null items.

Spring GraphQL Schema Inspection

Following, let’s rerun our application.

As a result, we should get the following in logs:

o.s.b.a.g.GraphQlAutoConfiguration: GraphQL schema inspection:
Unmapped fields: {Query=[article, articles], Mutation=[createArticle, addComment]}
Unmapped registrations: {}
Unmapped arguments: {}
Skipped types: []

As we can see, Spring Boot verifies the schema we defined every time we start the application. And the above message simply says that we do not have handler methods for our definition.

Let’s fix that then

Create Models

As the next step, let’s add Kotlin data classes to our Spring Boot project. They will be later used to serialize and deserialize data defined in our GraphQL schema.

To do so, let’s add the Models.kt:

data class User(
val id: String,
val name: String,
)

data class Article(
val id: String,
val title: String,
val content: String,
val authorId: String,
val createdAt: String,
)

data class Comment(
val id: String,
val content: String,
val articleId: String,
val userId: String,
)

data class CreateArticleInput(
val title: String,
val content: String,
val userId: String,
)

data class AddCommentInput(
val content: String,
val userId: String,
)

As we can see, nothing spectacular here. The only interesting fact is that the ID is serialized in the same way as a String.

QueryMapping as suspend fun

Following, let’s learn how the Spring for GraphQL works with Kotlin coroutines.

According to the documentation:

Kotlin coroutine and Flow are adapted to Mono and Flux.

Which means that whenever we use the Spring WebFlux, Spring automatically converts our suspended functions Mono and functions that return Flow to Flux .

Implement Article Service

Before we head to the GraphQL part, let’s make some small preparations.

Let’s insert the ArticleService:

@Component
object ArticleService {

private val articles = mutableListOf(
Article(
id = «article-id-1»,
title = «article-title-1»,
content = «article-content-1»,
authorId = «user-id-2»,
createdAt = LocalDateTime.now().toString()
),
Article(
id = «article-id-2»,
title = «article-title-2»,
content = «article-content-2»,
authorId = «user-id-2»,
createdAt = LocalDateTime.now().toString()
),
Article(
id = «article-id-3»,
title = «article-title-3»,
content = «article-content-3»,
authorId = «user-id-3»,
createdAt = LocalDateTime.now().toString()
),
)

suspend fun findArticleById(id: String): Article? {
delay(100)
return articles.firstOrNull { it.id == id }
}

}

As we can see, we use my favorite, in-memory database called mutable list () and expose findArticleById – a suspended function.

This function will either find the desired article by ID or return a null value.

Add ArticleController

With that done, let’s add the ArticleController:

@Controller
class ArticleController(
private val articleService: ArticleService,
) {

@QueryMapping
suspend fun article(@Argument id: String): Article? =
articleService.findArticleById(id)

}

As we can see, thanks to the Spring GraphQL, we can leverage the annotation-based approach.

Firstly, we must annotate our class with the @Controller annotation. Then, all the methods inside it annotated with @SchemaMapping annotation will become handlers.

And @QueryMapping, @MutationMapping, or @SubscriptionMapping are nothing else than meta annotations (annotated with @SchemaMapping). This way, we achieve a more readable code.

Additionally, in Spring, we use the @Argument annotation to bind GraphQL input arguments to our Kotlin instances.

Enable GraphiQL & Test

Nextly, let’s turn on the GraphiQL– the IDE for testing GraphQL APIs (like Postman, Bruno, or Insomnia).

To do so, let’s navigate to the resources directory and change the application.properties into the application.yaml file:

spring:
application:
name: graphsandbox
graphql:
graphiql:
enabled: true

Then, let’s open up the browser, go to http://localhost:8080/graphiql, and put the following:

query SomeRandomQuery {
article(id: «article-id-1») {
id
title
}
}

As we can see, the query name is up to us, but inside it, we must define which of the “exposed” queries we would like to use. As the input parameter, we pass the String value- article-id-1– and lastly, we define what fields we would like to get in response (that’s what the whole GraphQL is a about, right?).

So, as the next step, let’s run the query and check out the result:

{
«errors»: [
{
«message»: «INTERNAL_ERROR for f337f3f3-5»,
«locations»: [
{
«line»: 2,
«column»: 3
}
],
«path»: [
«article»
],
«extensions»: {
«classification»: «INTERNAL_ERROR»
}
}
],
«data»: {
«article»: null
}
}

Unfortunately, that is not what we expected.

Moreover, when we check out logs, we should see this:

s.g.e.ExceptionResolversExceptionHandler : Unresolved NoClassDefFoundError for executionId f337f3f3-5
java.lang.NoClassDefFoundError: org/springframework/data/util/KotlinReflectionUtils
at org.springframework.graphql.data.method.InvocableHandlerMethodSupport.invokeSuspendingFunction(InvocableHandlerMethodSupport.java:141) ~[spring-graphql-1.3.4.jar:1.3.4]
at org.springframework.graphql.data.method.InvocableHandlerMethodSupport.doInvoke(InvocableHandlerMethodSupport.java:108) ~[spring-graphql-1.3.4.jar:1.3.4]

Fix Spring GraphQL Coroutines Issue

As we could see, the issue is caused by the missing dependency, and we can easily fix that.

To do so, let’s open up the build.gradle.kts and add the following dependency:

implementation(«org.springframework.data:spring-data-commons»)

Then, let’s sync the gradle project and rerun the application.

After we run the test again, we should see the following:

{
«data»: {
«article»: {
«id»: «article-id-1»,
«title»: «article-title-1»
}
}
}

Splendid! Everything works perfectly fine

Spring GraphQL with Kotlin Flow

With that done, let’s learn how to work with Kotlin Flow and Spring GraphQL.

As the first step, let’s insert a new function to our ArticleService:

fun findAllArticles(): Flow<Article> = articles.asFlow()

This way, we produce a cold flow from our list of articles.

Then, let’s add a new handler:

@QueryMapping
fun articles(): Flow<Article> =
articleService.findAllArticles()

Nothing new this time. Just like previously, we add a new function marked with @QueryMapping.

When we run the following query in GraphiQL:

query AnotherOne {
articles {
id
title
createdAt
}
}

We should see the exact result:

{
«data»: {
«articles»: [
{
«id»: «article-id-1»,
«title»: «article-title-1»,
«createdAt»: «2025-04-12T13:06:29.142469200»
},
{
«id»: «article-id-2»,
«title»: «article-title-2»,
«createdAt»: «2025-04-12T13:06:29.142469200»
},
{
«id»: «article-id-3»,
«title»: «article-title-3»,
«createdAt»: «2025-04-12T13:06:29.142469200»
}
]
}
}

@SchemaMapping

One of the greatest thing in GraphQL is the possibility to return the related data in one, single query. And if you remember our schema definition, our API should allow us to do that.

To be more specific, each article can have the author, multiple comments, and each comment also has its author.

But when we test that functionality right now:

query AnotherOne {
articles {
id
title
createdAt
author {
id
name
}

comments {
id
content
author {
name
}
}
}
}

The only thing we will get in response will be a looooong list of errors:

{
«errors»: [
{
«message»: «The field at path ‘/articles[0]/author’ was declared as a non null type, but the code involved in retrieving data has wrongly returned a null value. The graphql specification requires that the parent field be set to null, or if that is non nullable that it bubble up null to its parent and so on. The non-nullable type is ‘User’ within parent type ‘Article'»,
«path»: [
«articles»,
0,
«author»
],
«extensions»: {
«classification»: «NullValueInNonNullableField»
}
}

… other errors

Of course, with Spring we can easily fix it, but we will need small preparation first.

Update Service

Firstly, let’s get back to our service and update it:

@Component
object ArticleService {

private val articles = mutableListOf(
Article(
id = «article-id-1»,
title = «article-title-1»,
content = «article-content-1»,
authorId = «user-id-2»,
createdAt = LocalDateTime.now().toString()
),
Article(
id = «article-id-2»,
title = «article-title-2»,
content = «article-content-2»,
authorId = «user-id-2»,
createdAt = LocalDateTime.now().toString()
),
Article(
id = «article-id-3»,
title = «article-title-3»,
content = «article-content-3»,
authorId = «user-id-3»,
createdAt = LocalDateTime.now().toString()
),
)

private val users = mutableListOf(
User(id = «user-id-1», name = «user-name-1»),
User(id = «user-id-2», name = «user-name-2»),
User(id = «user-id-3», name = «user-name-3»),
)

private val comments = mutableListOf(
Comment(id = «comment-id-1», content = «comment-content-1», articleId = «article-id-1», userId = «user-id-3»),
Comment(id = «comment-id-2», content = «comment-content-2», articleId = «article-id-2», userId = «user-id-3»),
Comment(id = «comment-id-3», content = «comment-content-3», articleId = «article-id-2», userId = «user-id-2»),
Comment(id = «comment-id-4», content = «comment-content-4», articleId = «article-id-3», userId = «user-id-3»),
)

suspend fun findArticleById(id: String): Article? {
delay(100)
return articles.firstOrNull { it.id == id }
}

fun findAllArticles(): Flow<Article> = articles.asFlow()

suspend fun findUserById(id: String): User? {
delay(100)
return users.firstOrNull { it.id == id }
}

fun findCommentsByArticleId(id: String): Flow<Comment> =
comments.filter { it.articleId == id }.asFlow()
}

As we can see, we added two more “tables” along with simple functions to obtain data from them.

Update @Controller

Then, let’s update our controller class:

@SchemaMapping
suspend fun author(article: Article): User =
articleService.findUserById(article.authorId)!!

@SchemaMapping
suspend fun author(comment: Comment): User =
articleService.findUserById(comment.userId)!!

@SchemaMapping
fun comments(article: Article): Flow<Comment> =
articleService.findCommentsByArticleId(article.id)

As we can see, this time we leverage the @SchemaMapping annotation for our handlers. Pretty similar to what we did already.

The interesting part here is the parameters. Every handler takes the parent type as an argument. Meaning, that for the article -> author parent-child relationship, we define the function that takes the article instance as an argument. And that’s it!

Of course, please the double exclamation (!!) should not land in the real, production-ready code

Anyway, when we rerun our test now, we will see the following:

{
«data»: {
«articles»: [
{
«id»: «article-id-1»,
«title»: «article-title-1»,
«createdAt»: «2025-04-12T13:21:17.584507600»,
«author»: {
«id»: «user-id-2»,
«name»: «user-name-2»
},
«comments»: [
{
«id»: «comment-id-1»,
«content»: «comment-content-1»,
«author»: {
«name»: «user-name-3»
}
}
]
}

… more thingies

And that’s what we expected. Awesome!

GraphQL Mutations

When we run the application right now, we see that we still miss two things that we defined- mutations.

So, before we add a new handler, let’s implement the following function in our service:

suspend fun createArticle(input: CreateArticleInput): Article {
delay(100)

return Article(
id = UUID.randomUUID().toString(),
title = input.title,
content = input.content,
authorId = input.userId,
createdAt = LocalDateTime.now().toString(),
).also { articles.add(it) }
}

As can be seen, we take the input, create a new Article instance, and insert that to the list.

With Kotlin also ,we can return at the same time the created instance and achieve a slightly cleaner code. To be even fancier, we could use a reference here: .also(articles::add)

Then, let’s add the appropriate handler to our controller:

@MutationMapping
suspend fun createArticle(@Argument input: CreateArticleInput): Article =
articleService.createArticle(input)

As we can see, just like with @QueryMapping, this time, we mark the handler with @MutationMapping and our argument with @Argument. Nothing else is necessary to make our mutation work.

So, given that, let’s rerun the app and test our functionality:

mutation someMutation {
createArticle(
input: {
title: «Awesome codersee Kotlin article»,
content: «Some content»,
userId: «user-id-3»
}
) {
id
title
content
author {
id
name
}
}
}

As a result, we should get the following:

{
«data»: {
«createArticle»: {
«id»: «0e51a902-835b-4e55-9b4a-f2dccd242ea7»,
«title»: «Awesome codersee Kotlin article»,
«content»: «Some content»,
«author»: {
«id»: «user-id-3»,
«name»: «user-name-3»
}
}
}
}

Wonderful! We can see that with GraphQL mutations, we can not only create/modify things on our server. With this one query, we can also retrieve the data and the dependent objects

Error Handling in Spring GraphQL

As the last step, let’s add one more mutation to learn more about error handling.

First, let’s add the new custom exception ot our codebase in Models.kt:

data class GenericNotFound(val msg: String) : RuntimeException(msg)

Then, let’s implement the following function in ArticleService:

suspend fun addComment(id: String, input: AddCommentInput): Comment {
delay(100)

users.firstOrNull { it.id == input.userId }
?: throw GenericNotFound(«User not found»)

return articles.firstOrNull { it.id == id }
?.let {
Comment(
id = UUID.randomUUID().toString(),
articleId = id,
content = input.content,
userId = input.userId,
).also { comments.add(it) }
}
?: throw GenericNotFound(«Article not found»)

}

This time, we can see a more production-ready code.

If we want to add a comment to the article, we must first check if both the desired author and article exist, right?

If that is not the case, then we throw our custom exception.

Following, let’s add a new mutation handler:

@MutationMapping
suspend fun addComment(
@Argument id: String,
@Argument input: AddCommentInput,
): Comment =
articleService.addComment(id, input)

Now, when we rerun the app and execute the following test:

mutation anotherMutation {
addComment(
articleId: «non-existing»
input: {userId: «user-id-1», content: «My comment»}
) {
id
content
}
}

We will see the following error:

{
«errors»: [
{
«message»: «INTERNAL_ERROR for f62a4c7e-1»,
«locations»: [
{
«line»: 52,
«column»: 3
}
],
«path»: [
«addComment»
],
«extensions»: {
«classification»: «INTERNAL_ERROR»
}
}
],
«data»: {
«addComment»: null
}
}

And this is not what we wanted, right?

GraphQlExceptionHandler and ControllerAdvice

One of the solutions we can have in such a situation is a combination of ControllerAdvice and GraphQlExceptionHandler.

If you would like to learn more about ControllerAdvice and RestControllerAdvice, then check out my other article.

So, as the next step, let’s add the GlobalExceptionHandler class to the project:

@ControllerAdvice
class GlobalExceptionHandler {

@GraphQlExceptionHandler
fun handleGenericNotFound(ex: GenericNotFound): GraphQLError =
GraphQLError.newError()
.errorType(ErrorType.DataFetchingException)
.message(ex.msg)
.build()
}

As we can see, inside it, we implement a function that takes the GenericNotFound as an argument. This way, whenever our custom exception is thrown, Spring will return the GraphQLError instance with our config instead.

As a result, when we retest the application, we should get the following JSON:

{
«errors»: [
{
«message»: «Article not found»,
«locations»: [],
«extensions»: {
«classification»: «DataFetchingException»
}
}
],
«data»: {
«addComment»: null
}
}

Spring GraphQL with Kotlin Coroutines Summary

And that’s all for this article on how to work with Kotlin coroutines and Flows in the Spring Boot GraphQL project.

As always, you can find the source code in my GitHub repostory.

The post Spring for GraphQL with Kotlin Coroutines appeared first on Codersee — Kotlin on the backend.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *