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:
// 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:
- Schema: Defines the types and operations available in your API
- Queries: Read operations to fetch data
- Mutations: Write operations to modify data
- Types: Define the structure of your data
- 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:
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:
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:
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:
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:
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:
query {
books {
id
title
publishedYear
}
}
Response:
{
"data": {
"books": [
{
"id": "1",
"title": "Kotlin in Action",
"publishedYear": 2017
},
{
"id": "2",
"title": "Effective Kotlin",
"publishedYear": 2019
}
]
}
}
Fetching a specific book with its author:
query {
booksWithAuthors {
id
title
publishedYear
author {
name
biography
}
}
}
Adding a new book:
mutation {
addBook(
title: "Programming Kotlin",
authorId: "2",
publishedYear: 2020
) {
id
title
}
}
Response:
{
"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:
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:
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:
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:
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:
// 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:
- Setting up a Kotlin GraphQL server with graphql-kotlin
- Defining GraphQL schemas with Kotlin classes and annotations
- Implementing queries and mutations for data retrieval and modification
- Handling relationships between types
- Advanced features like DataLoaders for efficient data fetching
- Error handling and security considerations
- 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
- GraphQL Official Documentation
- GraphQL-Kotlin GitHub Repository
- GraphQL-Kotlin Documentation
- Apollo Kotlin Client (For consuming GraphQL APIs from Kotlin clients)
Exercises
- Extend the library management system to include a "reserveBook" mutation that allows members to reserve books that are currently unavailable.
- Implement filtering capabilities to search books by title or author name.
- Add pagination to the books query to handle large numbers of books efficiently.
- Create a subscription that notifies when a reserved book becomes available.
- 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! :)