Overview
The Ktor Starter demonstrates how to integrate Viaduct with Ktor, a modern Kotlin-native asynchronous web framework. This tutorial is ideal for developers who want to leverage Ktor’s coroutine-first design with Viaduct’s GraphQL capabilities.What You’ll Learn
- Integrating Viaduct with Ktor server
- Setting up GraphQL and GraphiQL routes
- Handling GraphQL requests using Ktor’s routing DSL
- Working with Ktor’s asynchronous request/response model
- Configuring Viaduct with Ktor plugins
Prerequisites
- Java JDK 21 installed
JAVA_HOMEenvironment variable set correctly orjavain your PATH- Basic familiarity with Kotlin coroutines
Project Structure
package com.example.viadapp
import io.ktor.server.application.Application
const val SCHEMA_ID: String = "publicSchema"
fun main(argv: Array<String>) {
io.ktor.server.jetty.EngineMain.main(argv)
}
fun Application.module() {
configurePlugins()
configureRouting()
}
EngineMain for server startupApplication.module() extension function configures the appfun Application.configureRouting() {
val viaduct = ViaductConfiguration.viaductService
routing {
// GraphiQL UI endpoint
get("/graphiql") {
val resource = this::class.java.classLoader.getResource("graphiql/index.html")
if (resource != null) {
call.respondText(resource.readText(), ContentType.Text.Html)
} else {
call.respond(HttpStatusCode.NotFound, "GraphiQL not found")
}
}
// GraphQL endpoint
route("/graphql") {
post {
val request = call.receive<Map<String, Any?>>()
// Validate query parameter
val query = request["query"] as? String
if (query == null) {
call.respond(
HttpStatusCode.BadRequest,
mapOf("errors" to listOf(mapOf("message" to "Query parameter is required")))
)
return@post
}
// Create execution input
val executionInput = ExecutionInput.create(
operationText = query,
variables = (request["variables"] as? Map<String, Any>) ?: emptyMap(),
)
// Execute query asynchronously
val result: ExecutionResult = viaduct.executeAsync(executionInput).await()
// Return response
val statusCode = when {
result.errors.isNotEmpty() -> HttpStatusCode.BadRequest
else -> HttpStatusCode.OK
}
call.respond(statusCode, result.toSpecification())
}
}
}
}
call.receive<Map<String, Any?>>() automatically parses JSONviaduct.executeAsync().await() integrates with Kotlin coroutinesobject ViaductConfiguration {
val viaductService: Viaduct by lazy {
BasicViaductFactory.create(
tenantRegistrationInfo = TenantRegistrationInfo(
tenantPackagePrefix = "com.example.viadapp"
)
)
}
}
@Resolver
class GreetingResolver : QueryResolvers.Greeting() {
override suspend fun resolve(ctx: Context): String {
return "Hello, World!"
}
}
@Resolver
class AuthorResolver : QueryResolvers.Author() {
override suspend fun resolve(ctx: Context): String {
return "Brian Kernighan"
}
}
curl 'http://localhost:8080/graphql' \
-H 'content-type: application/json' \
--data-raw '{"query":"{ greeting }"}'
curl 'http://localhost:8080/graphql' \
-H 'content-type: application/json' \
--data-raw '{"query":"query HelloWorld { greeting author }"}'
How It Works
Ktor’s Coroutine Integration
Ktor is built on Kotlin coroutines, making it a natural fit for Viaduct’s suspend functions:viaduct.executeAsync()returns aCompletableFuture.await()(fromkotlinx.coroutines.future) converts it to a suspending call- No blocking threads—fully asynchronous execution
Request Processing Flow
- HTTP POST to
/graphql: Ktor receives the request - JSON Deserialization:
call.receive<Map<String, Any?>>()parses the body - Validation: Check that
queryfield exists - ExecutionInput Creation: Build Viaduct’s execution context
- Async Execution:
viaduct.executeAsync(executionInput).await() - Response: Convert to JSON and send with appropriate status code
Error Handling
The routing logic handles errors gracefully:- Missing query: Returns 400 Bad Request with error message
- GraphQL errors: Returns 400 with errors in response body
- Successful execution: Returns 200 OK
Configuration Files
Ktor usesapplication.conf for server configuration:
Why Use Ktor with Viaduct?
- Coroutine-First: Both Ktor and Viaduct embrace Kotlin coroutines
- Lightweight: Minimal overhead compared to traditional servlet containers
- Idiomatic Kotlin: Clean DSL for routing and configuration
- Async by Default: Non-blocking I/O for high-performance applications
- Modern Stack: Built for cloud-native and microservice architectures
Next Steps
- Explore the Micronaut Starter Tutorial for dependency injection
- Learn about Custom Context for request metadata
- Study the Star Wars Tutorial for complex schema design
- Read the Getting Started Guide for foundational concepts