Viaduct and ViaductBuilder

How the Viaduct runtime is constructed in the Star Wars demo using ViaductBuilder.

This page explains how the Viaduct runtime is built in the Star Wars demo, referencing the configuration code (for example, ViaductConfiguration.kt) and the controller that executes requests (ViaductGraphQLController.kt).

Goal: make it clear what the builder registers, how schemas are defined, and what the runtime looks like when it receives an ExecutionInput to resolve queries and mutations.

High-level flow

  1. Schema registration (IDs, SDL discovery, and scope sets).
  2. Module registration (generated types, resolvers, and package conventions).
  3. Runtime construction via ViaductBuilder.
  4. Execution: the controller creates an ExecutionInput (with schemaId, query, variables, etc.) and calls viaduct.executeAsync(...).

Builder configuration

This excerpt mirrors what happens in configuration (names and constants from the demo):

Tag not found: viaduct_configuration in demoapps/starwars/src/main/kotlin/com/example/starwars/service/viaduct/ViaductConfiguration.kt

  • PUBLIC_SCHEMA and PUBLIC_SCHEMA_WITH_EXTRAS are schema IDs used by the demo.
  • grtPackagePrefix and grtResourcesIncluded are optional test-only overrides for GRT schema file discovery (production uses defaults).
  • tenantPackagePrefix tells Viaduct where to discover generated resolver classes.
  • The builder creates an immutable runtime that the controller will use to execute requests.

Example: executing requests through the controller

The controller resolves scopes → chooses a schema → builds ExecutionInput → executes:

@Controller
class ViaductRestController(
 private val viaduct: Viaduct,
 private val securityAccessService: SecurityAccessContext
) {
 @Post("/graphql")
 suspend fun graphql(
     @Body request: Map<String, Any>,
     @Header(SCOPES_HEADER) scopesHeader: String?,
     @Header("security-access") securityAccess: String?
 ): HttpResponse<Map<String, Any>> {
     securityAccessService.setSecurityAccess(securityAccess)
     val executionInput = createExecutionInput(request)
     // tag::run_query[7] Runs the query example
     val scopes = parseScopes(scopesHeader)
     val schemaId = determineSchemaId(scopes)
     val result = viaduct.executeAsync(executionInput, schemaId).await()
     return HttpResponse.status<Map<String, Any>>(statusCode(result)).body(result.toSpecification())
 }

For details on determineSchemaId(scopes) and createExecutionInput(...), see the Scope and Schemas documentation in this set.

Builder best practices

  • Declare schema IDs and their scope sets explicitly.
  • Keep tenantPackagePrefix aligned with generated resolver code (com.example.starwars...).
  • Use default GRT schema discovery in production (don’t set grtPackagePrefix or grtResourcesIncluded).
  • Configure directives and modules in the builder when applicable.
  • Avoid conditional logic in the builder; route by scope in the controller instead.