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

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:

  1. GET /users/{id}
  2. GET /users/{id}/posts?limit=10
  3. 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!

  1. 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.

  1. The Main Application (Application.kt or similar)
    1. 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
}
  1. 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
    }
}
  1. 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.

  1. 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()
    }
}
  1. 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 by graphql-kotlin-ktor-server.
  • packages: Tell graphql-kotlin where to scan for classes that define your schema (Queries, Mutations, Subscriptions, and data types).
  • queries, mutations, subscriptions: You provide instances of classes that implement your GraphQL operations. These classes will contain functions that map to your GraphQL fields
  1. 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
}
GraphiQL

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!