Skip to content

Server Integration

Viaduct is a pure GraphQL execution engine. It does not include an HTTP server, a message broker client, or any other transport layer. Its contract is simple: call execute with an instance of ExecutionInput and get back an ExecutionResult.

// Build the input
val input = ExecutionInput.create(
    operationText = query,
    variables = variables,
    requestContext = yourRequestContext,
)

// Execute
val result = viaduct.execute(input)

// Format result as a GraphQL-spec-compliant JSON object
val responseBody = result.toSpecification()
ExecutionInput input = ExecutionInput.builder()
    .operationText(query)
    .variables(variables)
    .requestContext(yourRequestContext)
    .build();

CompletableFuture<Map<String, Object>> responseBody =
    viaduct.executeAsync(input).thenApply(result -> result.toSpecification());

Kotlin's execute() suspends in the caller's coroutine. Java's executeAsync returns a CompletableFuture<ExecutionResult>; compose the future with thenApply() and let your server complete the response asynchronously. For a scoped application, pass the registered schema ID as the second argument. See Viaduct API for overloads and result handling.

Constructing a Viaduct Instance

There are two ways to construct a Viaduct instance: using the simpler BasicViaductFactory and using the more feature-rich ViaductBuilder.

Both of these classes use resource files generated by Viaduct's application and module plugins to configure both the schema served by a Viaduct application and the resolvers that implement that schema. See Getting Started for a basic introduction to these plugins, and Multi-tenancy for more advanced uses.

ViaductBuilder provides other mechanisms for configuration, such as configuration of dependency injection and observability telemetry. These are covered by other subsections of this Service Engineering section.

Request-Scoped Data

The entry point that calls Viaduct is responsible for extracting request-scoped data such as caller identity, auth tokens, locale, and tenant information from the incoming transport request.

For production services, prefer putting this data into framework-managed request-scoped beans and consuming those beans from resolvers through Dependency Injection. This keeps resolver access type-safe and lets the web framework manage request lifetimes.

For simple integrations, ExecutionInput.requestContext can carry deployment-specific context directly into resolver execution. The value is exposed as ctx.requestContext (Any?) in Kotlin and context.getRequestContext() (Object, possibly null) in Java, so callers and resolvers must agree on the object shape. Values passed this way should be immutable or thread-safe because resolvers for the same operation may execute concurrently.

HTTP Embedding

Getting Started shows Ktor for Kotlin and Micronaut for Java. These Micronaut examples translate an HTTP request into an execution input. The Java example comes directly from the Star Wars demo and also validates the query, carries request context, and serves each scoped schema on its own route:

@Controller
class ViaductRestController(
    private val viaduct: Viaduct,
) {
    @Post("/graphql")
    suspend fun graphql(
        @Body request: Map<String, Any>,
    ): HttpResponse<Map<String, Any?>> {
        val input = ExecutionInput.create(
            operationText = request["query"] as String,
            variables = (request["variables"] as? Map<String, Any>) ?: emptyMap(),
        )
        val result = viaduct.execute(input)
        return HttpResponse.ok(result.toSpecification())
    }
}
@Controller
public final class ViaductRestController {
  private final Viaduct viaduct;
  private final ViaductConfiguration.StarWarsSchemas schemas;
  public ViaductRestController(Viaduct viaduct, ViaductConfiguration.StarWarsSchemas schemas) {
    this.viaduct = viaduct;
    this.schemas = schemas;
  }
  @Post("/graphql")
  public CompletableFuture<HttpResponse<Map<String, Object>>> graphql(
      @Body Map<String, Object> request,
      @Header("security-access") @Nullable String securityAccess) {
    return execute(request, schemas.defaultSchema().getSchemaId(), securityAccess);
  }
  @Post("/extras/graphql")
  public CompletableFuture<HttpResponse<Map<String, Object>>> extrasGraphql(
      @Body Map<String, Object> request,
      @Header("security-access") @Nullable String securityAccess) {
    return execute(request, schemas.extras().getSchemaId(), securityAccess);
  }
  private CompletableFuture<HttpResponse<Map<String, Object>>> execute(
      Map<String, Object> request, SchemaId schemaId, @Nullable String securityAccess) {
    Object queryValue = request.get("query");
    if (!(queryValue instanceof String query)) {
      return CompletableFuture.completedFuture(
          HttpResponse.badRequest(
              Map.of("errors", List.of(Map.of("message", "Missing 'query' field")))));
    }
    ExecutionInput executionInput =
        ExecutionInput.builder()
            .operationText(query)
            .variables(readVariables(request))
            .requestContext(
                Map.of(
                    SecurityAccessContext.REQUEST_CONTEXT_KEY,
                    new SecurityAccessContext(securityAccess)))
            .build();
    return viaduct
        .executeAsync(executionInput, schemaId)
        .thenApply(result -> HttpResponse.ok(result.toSpecification()));
  }

View full file on GitHub

Our Viaduct demo apps website contains integrations of Viaduct with a variety of web servers.

Messaging Embedding

Embedding Viaduct into a message-based transport is largely the same. IncomingMessage represents your transport adapter: it supplies the query, variables and headers, and a reply() method that sends the response. Import Viaduct, ExecutionInput, and Java's CompletableFuture as in the HTTP example:

class GraphQLMessageConsumer(
    private val viaduct: Viaduct,
) {
    suspend fun onMessage(message: IncomingMessage) {
        val input = ExecutionInput.create(
            operationText = message.query,
            variables = message.variables,
            requestContext = message.headers,
        )
        val result = viaduct.execute(input)
        message.reply(result.toSpecification())
    }
}
public final class GraphQLMessageConsumer {
    private final Viaduct viaduct;

    public GraphQLMessageConsumer(Viaduct viaduct) {
        this.viaduct = viaduct;
    }

    public CompletableFuture<Void> onMessage(IncomingMessage message) {
        ExecutionInput input = ExecutionInput.builder()
            .operationText(message.getQuery())
            .variables(message.getVariables())
            .requestContext(message.getHeaders())
            .build();
        return viaduct.executeAsync(input)
            .thenAccept(result -> message.reply(result.toSpecification()));
    }
}