Build From Scratch
This guide walks you through creating a Viaduct application from the ground up, giving you a deeper understanding of how Viaduct projects are structured.
Before you begin: Review the Compatibility section to choose an appropriate Gradle version and language toolchain for your project.
Getting Started¶
We'll build a simple CLI application that demonstrates the core concepts of Viaduct. This will be a single-module project where both the Viaduct application and schema are located inside the src directory.
Project Setup¶
Create a new directory for your project and navigate into it:
Configuring Gradle¶
Create settings.gradle.kts:
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("com.airbnb.viaduct.settings-gradle-plugin") version "3.0.0"
}
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
rootProject.name = "viaduct-hello-world"
includeViaductApplication {
project(":")
modulePackagePrefix("com.example.viadapp")
includeModule {
project(":")
modulePackageSuffix("resolvers")
}
}
The settings plugin registers the application and tenant module. Here both are in the same Gradle project. The package prefix and suffix put generated resolver bases in com.example.viadapp.resolvers.resolverbases.
Create build.gradle.kts:
plugins {
kotlin("jvm") version "2.3.21"
id("com.google.devtools.ksp") version "2.3.7"
id("com.airbnb.viaduct.application-gradle-plugin") version "3.0.0"
id("com.airbnb.viaduct.module-gradle-plugin") version "3.0.0"
application
}
dependencies {
implementation("com.airbnb.viaduct:api:3.0.0")
implementation("com.airbnb.viaduct:runtime:3.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.3")
implementation("ch.qos.logback:logback-classic:1.5.21")
runtimeOnly("org.jetbrains.kotlin:kotlin-reflect:2.3.21")
}
application {
mainClass.set("com.example.viadapp.ViaductApplicationKt")
}
Use the same Viaduct version for the settings, application, and module plugins and all Viaduct dependencies. Viaduct does not bundle kotlin-reflect or kotlinx-coroutines, so the application declares them.
Create settings.gradle.kts. This file uses Gradle’s Kotlin DSL even though the application is Java:
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("com.airbnb.viaduct.settings-gradle-plugin") version "3.0.0"
}
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
rootProject.name = "viaduct-hello-world"
includeViaductApplication {
project(":")
modulePackagePrefix("com.example.viadapp")
includeModule {
project(":")
modulePackageSuffix("resolvers")
}
}
The settings plugin registers the application and tenant module. Here both are in the same Gradle project. The package prefix and suffix put generated resolver bases in com.example.viadapp.resolvers.resolverbases.
Create build.gradle.kts:
plugins {
application
`java-library`
id("com.airbnb.viaduct.application-gradle-plugin") version "3.0.0"
id("com.airbnb.viaduct.module-java-gradle-plugin") version "3.0.0"
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
dependencies {
implementation("com.airbnb.viaduct:api:3.0.0")
implementation("com.airbnb.viaduct:runtime:3.0.0")
implementation("com.airbnb.viaduct:javaapi-api:3.0.0")
implementation("com.airbnb.viaduct:javaapi-runtime:3.0.0")
runtimeOnly("org.jetbrains.kotlin:kotlin-reflect:2.3.21")
runtimeOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
}
application {
mainClass.set("com.example.viadapp.ViaductApplication")
}
Use the same Viaduct version for the settings, application, and Java module plugins and all Viaduct dependencies. Viaduct does not bundle kotlin-reflect or kotlinx-coroutines, so the application declares them as runtime dependencies; you do not need Kotlin source files or the Kotlin JVM plugin.
Creating the Schema¶
Create the directory structure for your schema:
Create src/main/viaduct/schema/schema.graphqls with the following content:
Creating the Application Code¶
Create the directory structure for your Kotlin code:
Create src/main/kotlin/com/example/viadapp/ViaductApplication.kt:
package com.example.viadapp
import ch.qos.logback.classic.Level
import ch.qos.logback.classic.Logger
import com.fasterxml.jackson.databind.ObjectMapper
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import viaduct.service.BasicViaductFactory
import viaduct.service.api.ExecutionInput
fun main(argv: Array<String>) {
val rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as Logger
rootLogger.level = Level.ERROR
// Create a Viaduct engine using the BasicViaductFactory
// tag::building-viaduct[5] Building viaduct from BasicViaductFactory
val viaduct = BasicViaductFactory.create()
// Create an execution input
// tag::create-execution-input[12] Creating an execution input
val executionInput = ExecutionInput.create(
operationText = (
argv.getOrNull(0)
?: """
query {
greeting
}
""".trimIndent()
),
variables = emptyMap(),
)
// Run the query
// tag::viaduct-execute-operation[3] Execute a query through Viaduct.
val result = runBlocking {
viaduct.execute(executionInput)
}
// [toSpecification] converts to JSON as described in the GraphQL
// specification.
val mapper = ObjectMapper().writerWithDefaultPrettyPrinter()
println(
mapper.writeValueAsString(result.toSpecification())
)
}
Create src/main/kotlin/com/example/viadapp/resolvers/HelloWorldResolvers.kt:
package com.example.viadapp.resolvers
import com.example.viadapp.resolvers.resolverbases.QueryResolvers
import viaduct.api.resolver.Resolver
@Resolver
class GreetingResolver : QueryResolvers.Greeting() {
override suspend fun resolve(ctx: Context) = "Hello, World!"
}
@Resolver
class AuthorResolver : QueryResolvers.Author() {
override suspend fun resolve(ctx: Context) = "Brian Kernighan"
}
Create the Java source directories:
Create src/main/java/com/example/viadapp/ViaductApplication.java:
package com.example.viadapp;
import viaduct.service.BasicViaductFactory;
import viaduct.service.api.ExecutionInput;
import viaduct.service.api.SchemaId;
import viaduct.service.api.Viaduct;
public final class ViaductApplication {
public static void main(String[] args) {
Viaduct viaduct = BasicViaductFactory.create();
String query = args.length > 0 ? args[0] : "{ greeting }";
ExecutionInput input = ExecutionInput.builder()
.operationText(query)
.build();
var result = viaduct.executeAsync(input, SchemaId.Base.INSTANCE).join();
System.out.println(result.toSpecification());
}
}
Create src/main/java/com/example/viadapp/resolvers/GreetingResolver.java:
package com.example.viadapp.resolvers;
import com.example.viadapp.resolvers.resolverbases.QueryResolvers;
import java.util.concurrent.CompletableFuture;
import viaduct.java.api.annotations.Resolver;
@Resolver
public final class GreetingResolver extends QueryResolvers.Greeting {
@Override
public CompletableFuture<String> resolve(Context context) {
return CompletableFuture.completedFuture("Hello, world!");
}
}
Create src/main/java/com/example/viadapp/resolvers/AuthorResolver.java:
package com.example.viadapp.resolvers;
import com.example.viadapp.resolvers.resolverbases.QueryResolvers;
import java.util.concurrent.CompletableFuture;
import viaduct.java.api.annotations.Resolver;
@Resolver
public final class AuthorResolver extends QueryResolvers.Author {
@Override
public CompletableFuture<String> resolve(Context context) {
return CompletableFuture.completedFuture("Viaduct");
}
}
The Java module plugin generates QueryResolvers from the schema before Java compilation. The default factory constructs these public no-argument resolvers; this small CLI does not need a dependency injection framework. join() is used only at the CLI boundary; compose futures inside resolvers instead of blocking.
Building and running the Application¶
Build your application:
Run the application:
orUse an installed Gradle to create the wrapper, then build and run with JDK 21+:
gradle wrapper --gradle-version 9.1.0
./gradlew build
./gradlew -q run --args="'{ greeting }'"
./gradlew -q run --args="'{ author }'"
The CLI prints the GraphQL response as a Java map, for example {data={greeting=Hello, world!}}. The Gradle build runs Java code generation and annotation processing automatically.
What's Next¶
Continue to Touring the Application to understand the structure of a Viaduct application.