Skip to main content

Kotlin GraphQL

Introduction

GraphQL is a query language for APIs and a runtime for executing those queries against your data. Unlike traditional REST APIs where endpoints return fixed data structures, GraphQL allows clients to request exactly the data they need. This makes it highly efficient and flexible, especially for mobile applications and complex UIs.

In this tutorial, we'll explore how to implement GraphQL in Kotlin backend applications. We'll cover the core concepts of GraphQL, set up a GraphQL server using popular libraries, and build a complete example with queries, mutations, and data fetching.

Why GraphQL with Kotlin?

Kotlin's concise syntax and powerful features make it an excellent choice for implementing GraphQL APIs:

  • Null safety helps prevent common errors when handling optional GraphQL fields
  • Data classes match perfectly with GraphQL types
  • Coroutines provide elegant handling of asynchronous operations
  • Extension functions allow for clean code organization

Getting Started with GraphQL in Kotlin

Setting Up Dependencies

We'll use GraphQL-Kotlin, a library that makes it easy to create GraphQL servers in Kotlin. Let's set up our project with Gradle:

kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "1.8.0"
application
}

dependencies {
// GraphQL Kotlin libraries
implementation("com.expediagroup:graphql-kotlin-server:6.2.5")

// Spring Boot (optional, for Spring Boot integration)
implementation("org.springframework.boot:spring-boot-starter-web:2.7.8")
implementation("com.expediagroup:graphql-kotlin-spring-server:6.2.5")

// Testing dependencies
testImplementation(kotlin("test"))
}

Basic Concepts

Before diving into code, let's understand some GraphQL fundamentals:

  1. Schema: Defines the types and operations available in your API
  2. Queries: Read operations to fetch data
  3. Mutations: Write operations to modify data
  4. Types: Define the structure of your data
  5. Resolvers: Functions that fetch the data for fields

Creating Your First GraphQL Schema in Kotlin

Let's create a simple book library API. We'll define types for books and authors, and operations to query and modify them.

Defining Data Models

First, let's create our domain models:

kotlin
data class Book(
val id: String,
val title: String,
val authorId: String,
val publishedYear: Int
)

data class Author(
val id: String,
val name: String,
val biography: String? = null
)

Creating GraphQL Schema Classes

Now, let's define our GraphQL schema using annotations from graphql-kotlin:

kotlin
import com.expediagroup.graphql.server.operations.Query
import com.expediagroup.graphql.server.operations.Mutation

class BookQuery : Query {
private val bookService = BookService()

fun books(): List<Book> = bookService.getAllBooks()

fun bookById(id: String): Book? = bookService.getBookById(id)
}

class AuthorQuery : Query {
private val authorService = AuthorService()

fun authors(): List<Author> = authorService.getAllAuthors()

fun authorById(id: String): Author? = authorService.getAuthorById(id)
}

class BookMutation : Mutation {
private val bookService = BookService()

fun addBook(title: String, authorId: String, publishedYear: Int): Book {
return bookService.addBook(title, authorId, publishedYear)
}

fun deleteBook(id: String): Boolean {
return bookService.deleteBook(id)
}
}

Service Classes

Let's implement simple in-memory services for our example:

kotlin
class BookService {
private val books = mutableListOf(
Book("1", "Kotlin in Action", "1", 2017),
Book("2", "Effective Kotlin", "2", 2019)
)

fun getAllBooks(): List<Book> = books

fun getBookById(id: String): Book? = books.find { it.id == id }

fun addBook(title: String, authorId: String, publishedYear: Int): Book {
val newBook = Book(
id = (books.size + 1).toString(),
title = title,
authorId = authorId,
publishedYear = publishedYear
)
books.add(newBook)
return newBook
}

fun deleteBook(id: String): Boolean {
val initialSize = books.size
books.removeIf { it.id == id }
return books.size < initialSize
}
}

class AuthorService {
private val authors = mutableListOf(
Author("1", "Dmitry Jemerov"),
Author("2", "Marcin Moskala", "Kotlin trainer and consultant")
)

fun getAllAuthors(): List<Author> = authors

fun getAuthorById(id: String): Author? = authors.find { it.id == id }
}

Setting Up a GraphQL Server

Now let's create a server to expose our GraphQL API. We'll use Spring Boot with graphql-kotlin-spring-server:

kotlin
import com.expediagroup.graphql.server.operations.Query
import com.expediagroup.graphql.server.operations.Mutation
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@SpringBootApplication
class BookLibraryApplication

fun main(args: Array<String>) {
runApplication<BookLibraryApplication>(*args)
}

@Configuration
class GraphQLConfiguration {
@Bean
fun bookQuery() = BookQuery()

@Bean
fun authorQuery() = AuthorQuery()

@Bean
fun bookMutation() = BookMutation()
}

By default, the GraphQL server will be accessible at http://localhost:8080/graphql.

Relationships Between Types

One of GraphQL's strengths is handling relationships between objects. Let's extend our schema to include relationships between books and authors:

kotlin
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
import graphql.schema.DataFetchingEnvironment
import java.util.concurrent.CompletableFuture

data class BookWithAuthor(
val id: String,
val title: String,
val publishedYear: Int,
val author: Author
)

class BookWithRelationsQuery : Query {
private val bookService = BookService()
private val authorService = AuthorService()

fun booksWithAuthors(): List<BookWithAuthor> {
val books = bookService.getAllBooks()
return books.map { book ->
val author = authorService.getAuthorById(book.authorId)!!
BookWithAuthor(book.id, book.title, book.publishedYear, author)
}
}
}

Testing GraphQL Queries

Once your server is running, you can test it using various GraphQL clients or tools like GraphiQL.

Example Queries

Here are some example queries you can run:

Fetching all books:

graphql
query {
books {
id
title
publishedYear
}
}

Response:

json
{
"data": {
"books": [
{
"id": "1",
"title": "Kotlin in Action",
"publishedYear": 2017
},
{
"id": "2",
"title": "Effective Kotlin",
"publishedYear": 2019
}
]
}
}

Fetching a specific book with its author:

graphql
query {
booksWithAuthors {
id
title
publishedYear
author {
name
biography
}
}
}

Adding a new book:

graphql
mutation {
addBook(
title: "Programming Kotlin",
authorId: "2",
publishedYear: 2020
) {
id
title
}
}

Response:

json
{
"data": {
"addBook": {
"id": "3",
"title": "Programming Kotlin"
}
}
}

Advanced Features

DataLoaders for Efficient Data Fetching

When dealing with relationships in GraphQL, you might encounter the "N+1 query problem" where fetching a list of items with related data can result in many individual database queries.

DataLoaders help solve this by batching and caching requests. Here's how to implement them with graphql-kotlin:

kotlin
import com.expediagroup.graphql.dataloader.KotlinDataLoader
import org.dataloader.DataLoader
import org.dataloader.DataLoaderFactory
import java.util.concurrent.CompletableFuture

class AuthorDataLoader(private val authorService: AuthorService) : KotlinDataLoader<String, Author> {
override val dataLoaderName = "AUTHOR_DATALOADER"

override fun getDataLoader(): DataLoader<String, Author> {
return DataLoaderFactory.newDataLoader { authorIds ->
CompletableFuture.supplyAsync {
authorService.getAuthorsByIds(authorIds)
}
}
}
}

// Extended AuthorService
class AuthorService {
// ... existing code

fun getAuthorsByIds(ids: List<String>): List<Author> {
return ids.mapNotNull { id -> authors.find { it.id == id } }
}
}

Then update our Book class to use the DataLoader:

kotlin
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
import graphql.schema.DataFetchingEnvironment
import java.util.concurrent.CompletableFuture

data class Book(
val id: String,
val title: String,
val authorId: String,
val publishedYear: Int
) {
fun author(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<Author> {
return dataFetchingEnvironment.getValueFromDataLoader("AUTHOR_DATALOADER", authorId)
}
}

Error Handling

Proper error handling is crucial in GraphQL APIs. GraphQL-Kotlin provides several ways to handle errors:

kotlin
import com.expediagroup.graphql.server.exception.GraphQLKotlinException

class BookMutation : Mutation {
private val bookService = BookService()

fun addBook(title: String, authorId: String, publishedYear: Int): Book {
// Validate input
if (title.isBlank()) {
throw GraphQLKotlinException("Book title cannot be empty")
}

val author = authorService.getAuthorById(authorId)
?: throw GraphQLKotlinException("Author with ID $authorId does not exist")

return bookService.addBook(title, authorId, publishedYear)
}
}

Authentication and Authorization

For securing your GraphQL API, you can use Spring Security alongside graphql-kotlin:

kotlin
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain

@EnableWebSecurity
class SecurityConfig {

@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
return http.csrf().disable()
.authorizeRequests {
it.antMatchers("/graphql").permitAll()
.anyRequest().authenticated()
}
.build()
}
}

Real-World Example: Building a Library Management System

Let's put everything together to build a more complete example of a library management system:

kotlin
// Domain models
data class Book(
val id: String,
val title: String,
val authorId: String,
val genre: String,
val publishedYear: Int,
val available: Boolean = true
)

data class Author(
val id: String,
val name: String,
val biography: String? = null,
val nationality: String? = null
)

data class Member(
val id: String,
val name: String,
val email: String
)

data class Loan(
val id: String,
val bookId: String,
val memberId: String,
val loanDate: LocalDate,
val dueDate: LocalDate,
val returnDate: LocalDate? = null
)

// GraphQL Query classes
class LibraryQuery : Query {
private val bookService = BookService()
private val authorService = AuthorService()
private val memberService = MemberService()
private val loanService = LoanService()

fun books(genre: String? = null): List<Book> {
return if (genre != null) {
bookService.getBooksByGenre(genre)
} else {
bookService.getAllBooks()
}
}

fun authors(): List<Author> = authorService.getAllAuthors()

fun members(): List<Member> = memberService.getAllMembers()

fun activeLoans(): List<Loan> = loanService.getActiveLoans()
}

// GraphQL Mutation classes
class LibraryMutation : Mutation {
private val bookService = BookService()
private val memberService = MemberService()
private val loanService = LoanService()

fun addBook(
title: String,
authorId: String,
genre: String,
publishedYear: Int
): Book {
return bookService.addBook(title, authorId, genre, publishedYear)
}

fun borrowBook(bookId: String, memberId: String, days: Int): Loan {
val book = bookService.getBookById(bookId)
?: throw GraphQLKotlinException("Book not found")

if (!book.available) {
throw GraphQLKotlinException("Book is not available")
}

val member = memberService.getMemberById(memberId)
?: throw GraphQLKotlinException("Member not found")

// Update book availability
bookService.updateBookAvailability(bookId, false)

// Create loan record
return loanService.createLoan(bookId, memberId, days)
}

fun returnBook(loanId: String): Loan {
val loan = loanService.getLoanById(loanId)
?: throw GraphQLKotlinException("Loan not found")

if (loan.returnDate != null) {
throw GraphQLKotlinException("Book already returned")
}

// Update book availability
bookService.updateBookAvailability(loan.bookId, true)

// Update loan with return date
return loanService.returnLoan(loanId)
}
}

Summary

In this tutorial, we've covered the essentials of implementing GraphQL APIs with Kotlin:

  1. Setting up a Kotlin GraphQL server with graphql-kotlin
  2. Defining GraphQL schemas with Kotlin classes and annotations
  3. Implementing queries and mutations for data retrieval and modification
  4. Handling relationships between types
  5. Advanced features like DataLoaders for efficient data fetching
  6. Error handling and security considerations
  7. A real-world example of a library management system

GraphQL provides a flexible and efficient way to build APIs, and Kotlin's language features make implementing these APIs clean and concise. The combination delivers powerful backend services that can adapt to changing frontend requirements without constant API versioning.

Additional Resources

Exercises

  1. Extend the library management system to include a "reserveBook" mutation that allows members to reserve books that are currently unavailable.
  2. Implement filtering capabilities to search books by title or author name.
  3. Add pagination to the books query to handle large numbers of books efficiently.
  4. Create a subscription that notifies when a reserved book becomes available.
  5. Implement input validation for all mutations using custom validators.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)