Skip to content

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:

mkdir viaduct-hello-world
cd viaduct-hello-world

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:

mkdir -p src/main/viaduct/schema

Create src/main/viaduct/schema/schema.graphqls with the following content:

extend type Query {
  greeting: String @resolver
  author: String @resolver
}

Creating the Application Code

Create the directory structure for your Kotlin code:

mkdir -p src/main/kotlin/com/example/viadapp
mkdir -p src/main/kotlin/com/example/viadapp/resolvers

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())
    )
}

View full file on GitHub

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:

mkdir -p src/main/java/com/example/viadapp/resolvers

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:

gradle wrapper --gradle-version 8.14
./gradlew build

Run the application:

./gradlew -q run --args="'{ greeting }'"
or
./gradlew -q run --args="'{ author }'"

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