Scaffold
This guide walks you through creating a Viaduct application using the scaffold task, which generates a complete Ktor + Viaduct GraphQL project from a single command.
Getting Started¶
The scaffold task generates a ready-to-run Ktor application with:
- GraphQL endpoint (
/graphql) - GraphiQL UI (
/graphiql) - Health check endpoint (
/health) - A sample "hello world" resolver
Prerequisites¶
- Java 21 must be on the path or available via JAVA_HOME
- Gradle must be installed (the scaffold will generate wrapper files)
Running the Scaffold¶
First, clone a repository that has the Viaduct application plugin applied. The simplest is the CLI starter:
Then run the scaffold task with your desired package prefix:
This creates a complete project in ../my-app/.
Running the Generated Application¶
Navigate to your new project and run it:
The server starts with:
- GraphQL endpoint at http://localhost:8080/graphql
- GraphiQL UI at http://localhost:8080/graphiql
- Health check at http://localhost:8080/health
Try the greeting query in GraphiQL:
Generated Project Structure¶
my-app/
├── build.gradle.kts
├── settings.gradle.kts
├── gradle.properties
├── gradlew
├── gradlew.bat
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── src/
└── main/
├── kotlin/
│ └── com/example/myapp/
│ ├── Main.kt
│ ├── ktorplugins/
│ │ ├── ContentNegotiation.kt
│ │ └── Routing.kt
│ └── resolvers/
│ └── GreetingResolver.kt
├── resources/
│ └── application.conf
└── viaduct/
└── schema/
└── schema.graphqls
Key Generated Files¶
Main.kt¶
The entry point uses Ktor's EngineMain. Here's a similar pattern from the ktor-starter:
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()
schema.graphqls¶
The GraphQL schema extends the base Query type with a greeting field. The @resolver directive indicates that a resolver class implements this field:
resolvers/GreetingResolver.kt¶
A resolver implements the greeting query. Here's the pattern from the cli-starter:
@Resolver
class GreetingResolver : QueryResolvers.Greeting() {
override suspend fun resolve(ctx: Context): String {
return "Hello, World!"
}
}
ktorplugins/ContentNegotiation.kt¶
Configures Jackson for JSON serialization:
package com.example.viadapp
import com.fasterxml.jackson.databind.SerializationFeature
import io.ktor.serialization.jackson.jackson
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.application.pluginOrNull
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
fun Application.configurePlugins() {
if (pluginOrNull(ContentNegotiation) == null) {
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
}
}
}
}
ktorplugins/Routing.kt¶
Configures the GraphQL endpoint using BasicViaductFactory. The scaffold generates routes for /health, /graphql and /graphiql:
package com.example.viadapp
import com.example.viadapp.injector.ViaductConfiguration
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import kotlinx.coroutines.future.await
import viaduct.service.api.ExecutionInput
import viaduct.service.api.ExecutionResult
fun Application.configureRouting() {
val viaduct = ViaductConfiguration.viaductService
routing {
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")
}
}
route("/graphql") {
post {
@Suppress("UNCHECKED_CAST")
val request = call.receive<Map<String, Any?>>() as 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 and must be a string")))
)
return@post
}
@Suppress("UNCHECKED_CAST")
val executionInput = ExecutionInput.create(
operationText = query,
variables = (request["variables"] as? Map<String, Any>) ?: emptyMap(),
)
val result: ExecutionResult = viaduct.executeAsync(executionInput).await()
when {
Customizing Your Application¶
After scaffolding, the generated code is entirely yours to modify:
- Add types: Extend
schema.graphqlswith your types and queries - Add resolvers: Create new resolver classes in the
resolvers/package - Add routes: Extend
ktorplugins/Routing.ktwith custom endpoints - Configure the server: Modify
application.conffor port, host, etc.
Development Mode¶
For development with auto-restart on code changes:
What's Next¶
Continue to Touring the Application to understand the structure of a Viaduct application.