Tired of REST? Build Blazing-Fast GraphQL APIs with Kotlin & Ktor! ๐

๐ Are you finding traditional REST APIs a bit... clunky? Multiple endpoints for related data, over-fetching, under-fetching โ sound familiar? What if there was a more efficient, flexible, and developer-friendly way to build your APIs?
Enter GraphQL! ๐ And when you pair it with the conciseness of Kotlin and the lightweight power of the Ktor framework, you get a truly delightful API development experience.
In this article, we'll embark on a journey to build a robust GraphQL server from scratch using Kotlin and Ktor.
We won't just skim the surface; we'll build a practical example โ a backend for a YouTube video information and downloader tool. By the end, you'll understand the core concepts of GraphQL and have a solid foundation to build your own production-ready GraphQL APIs.
Ready to level up your backend game? Let's dive in!
This is a 4 Part Series (Estimated Read Time: ~18-20 minutes)
Part 1 : The core server setup using GraphQL-Kotlin Ktor Server by expediagroup .
Part 2 : Define Our Youtube Video Download API.
Part 3 : The core client setup for Kotlin Multiplatform App Android and JVM using Apollo Kotlin.
Part 4 : Consuming Our Api using Apollo Kotlin.
๐ค So, What's the Big Deal with GraphQL?
Before we jump into code, let's quickly understand why GraphQL is gaining so much traction.
Imagine you're building a screen in your app that needs user details, their last 10 posts, and their followers. With a traditional REST API, this might mean:
- GET /users/{id}
- GET /users/{id}/posts?limit=10
- GET /users/{id}/followers
That's three separate network requests! ๐ฉ And you might get way more data than you need (over-fetching) or have to make even more requests for related data (under-fetching).
GraphQL solves this with a single, smart endpoint. You, the client, ask for exactly what you need, and the server responds with just that data. No more, no less.
- Queries: For fetching data (like asking for that user, their posts, and followers in one go).
- Mutations: For creating, updating, or deleting data (think POST, PUT, DELETE in REST, but more structured).
- Subscriptions: For real-time updates (imagine getting live notifications when a download progresses โ we'll build this!).
Think of it like this:
- REST: Ordering a la carte from a massive menu, sometimes getting dishes you didn't fully want.
- GraphQL: Going to a buffet where you precisely pick only the items you want, in the exact quantities you need, all in one trip.
Why Kotlin + Ktor for GraphQL?
- Kotlin: Modern, concise, null-safe, and highly interoperable with Java. It makes for cleaner, more maintainable code.
- Ktor: A lightweight, asynchronous framework for building web applications in Kotlin. It's flexible and unopinionated, allowing us to integrate GraphQL smoothly.
- GraphQL-Kotlin: An excellent library by Expedia Group that makes setting up GraphQL servers in Kotlin incredibly easy, especially with Ktor.
๐ ๏ธ Project Setup: Laying the Foundation
We'll be building the backend for a YouTube video information and downloader. Let's get our tools ready!
- Dependencies (Your build.gradle.kts Magic)
First, we need to tell our project what libraries it needs. Open your build.gradle.kts file and add the following:
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.ktor)
alias(libs.plugins.kotlin.serialization)
application
}
dependencies {
// Ktor server essentials
implementation(libs.bundles.ktor.server) // Includes core, netty, websockets, cors
implementation(libs.bundles.ktor.json) // For JSON serialization
// GraphQL Kotlin - the star of our show!
implementation(libs.graphql.kotlin) // Specifically, graphql-kotlin-ktor-server
// Shared module (if you have one for data models)
// implementation(projects.shared) // Example
// Testing (always a good idea!)
testImplementation(libs.ktor.serverTestHost)
testImplementation(libs.kotlin.testJunit)
}
application {
mainClass.set("com.yourpackage.ApplicationKt") // Replace with your main class
}
What's happening here?
- ktor.server bundle: Gives us the Ktor server capabilities, including the Netty engine (for running the server), WebSockets (for Subscriptions), and CORS (Cross-Origin Resource Sharing for security).
- ktor.json bundle: Allows Ktor to understand and speak JSON using Kotlinx Serialization.
- graphql.kotlin: This is the graphql-kotlin-ktor-server library, which provides the glue to host a GraphQL schema within Ktor.
We also use a version catalog (libs.versions.toml) to manage our dependency versions cleanly. Here's a snippet:
toml
[versions]
kotlin = "2.2.10" # Or your current Kotlin version
ktor = "3.2.3" # Or your current Ktor version
graphql-kotlin = "9.0.0-alpha.8" # Or latest graphql-kotlin v9
[libraries]
graphql-kotlin = { group = "com.expediagroup", name = "graphql-kotlin-ktor-server", version.ref = "graphql-kotlin" }
# ... other Ktor libraries (ktor-server-core, ktor-server-netty, etc.)
ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors", version.ref = "ktor" }
ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets-jvm", version.ref = "ktor" }
ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation-jvm", version.ref = "ktor" }
[bundles]
ktor-server = ["ktor-server-core-jvm", "ktor-server-netty-jvm", "ktor-server-cors-jvm", "ktor-server-websockets-jvm"] # Example bundle
ktor-json = ["ktor-server-content-negotiation-jvm", "ktor-serialization-kotlinx-json-jvm"] # Example bundle
๐๏ธ Building the Server: From Zero to GraphQL Hero
With our dependencies in place, let's configure our Ktor server to speak GraphQL.
- The Main Application (Application.kt or similar)
- Ktor applications are organized into modules. Here's a typical structure:
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}
fun Application.module() {
// We'll configure everything here!
// configureDependencies() // If you use DI
configureSockets() // For real-time subscriptions
configureCORS() // For security
configureGraphQL() // The main event!
configureRouting() // To expose GraphQL endpoints
configureStatusPages() // For nice error handling
}
- WebSocket Configuration (for Subscriptions โจ)
To enable real-time updates with GraphQL Subscriptions, we need WebSockets
fun Application.configureSockets() {
val json = Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
encodeDefaults = true
}
install(WebSockets) {
pingPeriod = 1.seconds
contentConverter = KotlinxWebsocketSerializationConverter(json) // Use the shared Json instance
}
}
- CORS Configuration (Important for Web Clients!)
Cross-Origin Resource Sharing (CORS) is a security feature that browsers use. You'll need to configure it if your GraphQL API will be accessed by web applications running on different domains.
fun Application.configureCORS() {
install(CORS) {
// Allow requests from any host during development (be more restrictive in production!)
anyHost() // Or specify hosts: allowHost("client.example.com")
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Post)
allowMethod(HttpMethod.Get) // For GraphiQL and SDL
allowHeader(HttpHeaders.ContentType)
allowHeader(HttpHeaders.Authorization) // If you use auth
allowHeader("X-Requested-With")
allowHeader("apollographql-client-name") // For Apollo Studio
allowHeader("apollographql-client-version")
allowHeader("Apollo-Require-Preflight")
allowCredentials = false // true If you need to send cookies from cross-origin requests
maxAgeInSeconds = 3600
}
}
๐จ Security Note: anyHost() is fine for development, but in production, you should restrict this to known domains.
- Configure Status Pages
Configures default exception handling using Ktor Status Pages.
Returns following HTTP status codes:
- 405 (Method Not Allowed) - when attempting to execute mutation or query through a GET request
- 400 (Bad Request) - any other exception
private fun Application.configureStatusPages() {
install(StatusPages) {
defaultGraphQLStatusPages()
}
}
- The GraphQL Magic: configureGraphQL() ๐ช
This is where we integrate graphql-kotlin.
fun Application.configureGraphQL() {
install(GraphQL) {
schema {
packages = listOf("com.yourpackage.graphql.schema") // Package containing your GraphQL schema files
queries = listOf(
HelloWorldQuery()
)
mutations = listOf(
HelloWorldMutation()
)
subscriptions = listOf(
HelloWorldSubscription()
)
}
engine { // Optional: configure the engine
introspection {
enabled = true // Good for development, consider disabling in prod for security
}
}
server { // Optional: configure the server
// contextFactory = CustomGraphQLContextFactory() // If you need custom request context
}
}
}
What's happening here?
install(GraphQL)
: We install the GraphQL plugin provided bygraphql-kotlin-ktor-server
.packages
: Tellgraphql-kotlin
where to scan for classes that define yourschema
(Queries
,Mutations
,Subscriptions
, and datatypes
).queries
,mutations
,subscriptions
: You provide instances of classes that implement your GraphQL operations. These classes will contain functions that map to your GraphQL fields
- Routing: Exposing Your API
Now, let's define the HTTP routes for our GraphQL server.
import com.expediagroup.graphql.server.ktor.graphQLPostRoute
import com.expediagroup.graphql.server.ktor.graphiQLRoute
import com.expediagroup.graphql.server.ktor.graphQLSDLRoute
import com.expediagroup.graphql.server.ktor.graphQLSubscriptionsRoute
import io.ktor.server.routing.*
import io.ktor.server.http.content.* // For static playground files
fun Application.configureRouting() {
routing {
staticResources("playground", "playground")
// Standard GraphQL endpoint for GET and POST requests
graphQLGetRoute()
graphQLPostRoute()
// Endpoint for GraphQL Subscriptions (WebSocket)
graphQLSubscriptionsRoute("graphql")
// Interactive GraphiQL playground - great for testing!
graphiQLRoute()
// Expose the GraphQL Schema Definition Language (SDL)
graphQLSDLRoute()
// You can also serve a custom playground like Apollo Sandbox
// staticResources("/playground", "playground") // I will provide the files in next part you need to paste it in resources/playground
}
}
Now, We Make a Hello World Query to Test This Setup.
For That First We Will Define Schema: Queries, Mutations, and Subscriptions
GraphQL schema is the contract between your client and server. It defines the types of data that can be queried and the operations that can be performed.
Queries: Asking for Information ๐ฃ๏ธ
Queries
are used to fetch data. In graphql-kotlin
, you create a class
that implements com.expediagroup.graphql.server.operations.Query
.
class HelloWorldQuery : Query {
suspend fun helloWorld() = "Hello World!"
}
What's happening here?
- Each public function in your Query class becomes a field in your GraphQL Query type.
- Function parameters become GraphQL arguments.
- The return type of the function maps to the GraphQL return type.
- suspend functions are fully supported for asynchronous operations
๐ Running and Testing Your Server
Development Mode: ./gradlew :yourmodule:run
Testing with GraphiQL Playground
Once your server is running (by default on http://localhost:8080 if you used port 8080), Ktor and graphql-kotlin provide a fantastic in-browser IDE called GraphiQL.
Navigate to: http://localhost:8080/graphiql You'll see an interface where you can:
- Write GraphQL queries, mutations, and subscriptions.
- See your schema documentation on the right.
- Execute operations and see the JSON response.
- Pass variables for your operations.
Example Queries in GraphiQL:
query {
helloWorld
}

Phew! That's the core server setup. In Next Part, We will learn about Mutations and Subscriptions also define what our Youtube Video Download API can actually do.
If you enjoyed this post and want to learn more about Kotlin, consider subscribing to my newsletter. Youโll be the first to know about new articles, videos, and projects Iโm working on!