Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/airbnb/viaduct/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The Micronaut Starter demonstrates how to integrate Viaduct with Micronaut’s powerful dependency injection (DI) framework. This tutorial is essential for building production-ready applications where resolvers need access to services, repositories, and other dependencies.

What You’ll Learn

  • Integrating Viaduct with Micronaut’s DI container
  • Using dependency injection in GraphQL resolvers
  • Separating development and production code with Gradle source sets
  • Fast development mode with ViaductServer
  • Production-ready configuration without the development server

Prerequisites

  • Java JDK 21 installed
  • JAVA_HOME environment variable set correctly or java in your PATH
  • Understanding of dependency injection concepts

Project Structure

micronaut-starter/
├── src/
│   ├── main/kotlin/com/example/viadapp/production/
│   │   ├── ViaductConfiguration.kt         # Viaduct bean factory
│   │   └── MicronautTenantCodeInjector.kt  # DI bridge for resolvers
│   └── dev/kotlin/com/example/viadapp/dev/
│       └── MicronautViaductFactory.kt      # ViaductServer integration (dev only)
└── viadapp/                                 # Resolver module
    └── src/main/kotlin/com/example/viadapp/
        ├── HelloWorldResolver.kt
        └── HelloWorldTenantModule.kt

Key Concepts

Production vs Development

Production Build:
  • Includes only src/main/kotlin (production code)
  • Does NOT include viaduct-serve dependency
  • Suitable for deployment with a full HTTP server
Development Build:
  • Includes both src/main/kotlin and src/dev/kotlin
  • Includes viaduct-serve dependency for GraphiQL
  • Fast iteration with automatic schema reloading
1
Configure the Viaduct Bean Factory
2
The ViaductConfiguration.kt:22 creates a Viaduct instance as a Micronaut bean:
3
package com.example.viadapp.production

import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import viaduct.service.BasicViaductFactory
import viaduct.service.TenantRegistrationInfo
import viaduct.service.api.Viaduct

@Factory
class ViaductConfiguration(
    private val micronautTenantCodeInjector: MicronautTenantCodeInjector
) {
    @Bean
    fun providesViaduct(): Viaduct {
        return BasicViaductFactory.create(
            tenantRegistrationInfo = TenantRegistrationInfo(
                tenantPackagePrefix = "com.example.viadapp",
                tenantCodeInjector = micronautTenantCodeInjector
            )
        )
    }
}
4
Key Points:
5
  • @Factory marks this class as a bean factory
  • @Bean method provides the Viaduct instance
  • tenantCodeInjector enables DI for resolver classes
  • Micronaut automatically injects MicronautTenantCodeInjector
  • 6
    Implement the Tenant Code Injector
    7
    The MicronautTenantCodeInjector bridges Viaduct and Micronaut’s DI:
    8
    @Singleton
    class MicronautTenantCodeInjector(
        private val beanContext: BeanContext
    ) : TenantCodeInjector {
        override fun <T : Any> getInstance(clazz: KClass<T>): T {
            return beanContext.getBean(clazz.java)
        }
    }
    
    9
    This allows Viaduct to request instances from Micronaut’s container when creating resolvers.
    10
    Create Resolvers with Dependencies
    11
    Resolvers can now use constructor injection:
    12
    import jakarta.inject.Singleton
    
    @Singleton
    class GreetingService {
        fun getGreeting(): String = "Hello from Micronaut!"
    }
    
    @Resolver
    @Singleton
    class GreetingResolver(
        private val greetingService: GreetingService  // Injected dependency
    ) : QueryResolvers.Greeting() {
        override suspend fun resolve(ctx: Context): String {
            return greetingService.getGreeting()
        }
    }
    
    13
    Key Points:
    14
  • Both resolver and service are marked @Singleton
  • Dependencies are injected via constructor parameters
  • Micronaut manages the lifecycle of all beans
  • 15
    Set Up Development Mode
    16
    The MicronautViaductFactory.kt in src/dev/kotlin/ enables fast development:
    17
    import viaduct.serve.ViaductServerConfiguration
    import io.micronaut.context.ApplicationContext
    
    @ViaductServerConfiguration
    class MicronautViaductFactory : ViaductFactory {
        override fun create(): Viaduct {
            // Start only the Micronaut DI container (not the full HTTP server)
            val context = ApplicationContext.run()
            
            // Get the Viaduct instance from DI
            return context.getBean(Viaduct::class.java)
        }
    }
    
    18
    How It Works:
    19
  • ViaductServer discovers this provider via @ViaductServerConfiguration
  • Starts a minimal ApplicationContext (DI only, no HTTP server)
  • Retrieves the Viaduct bean from the container
  • Serves GraphQL requests with GraphiQL at http://localhost:8080
  • 20
    Configure Gradle Source Sets
    21
    The build.gradle.kts separates development and production code:
    22
    sourceSets {
        // Development-only source set
        create("dev") {
            kotlin.srcDir("src/dev/kotlin")
            compileClasspath += sourceSets.main.get().output
            runtimeClasspath += sourceSets.main.get().output
        }
    }
    
    // Dev configuration extends from main
    val devImplementation by configurations.getting {
        extendsFrom(configurations.implementation.get())
    }
    
    dependencies {
        // Production dependencies
        implementation("io.micronaut:micronaut-inject")
        implementation("io.micronaut:micronaut-context")
    
        // Development-only dependency
        devImplementation("com.airbnb.viaduct:viaduct-serve")
    }
    
    // Serve task includes dev classes
    tasks.named<JavaExec>("serve") {
        classpath += sourceSets["dev"].output
        classpath += sourceSets["dev"].runtimeClasspath
    }
    
    23
    Key Points:
    24
  • dev source set is separate from main
  • devImplementation dependencies are excluded from production builds
  • serve task explicitly includes dev classes and dependencies
  • 25
    Run in Development Mode
    26
    Start the development server with GraphiQL:
    27
    ./gradlew serve
    
    28
    This:
    29
  • Starts only the Micronaut DI container (not the full HTTP server)
  • Provides GraphiQL IDE at http://localhost:8080/graphiql
  • Faster startup than a full Micronaut HTTP server
  • 30
    Open your browser:
    31
    http://localhost:8080/graphiql
    
    32
    Run a query:
    33
    query {
      greeting
      author
    }
    
    34
    Enable Auto-Reload on Changes
    35
    Use Gradle’s continuous build mode:
    36
    ./gradlew --continuous serve
    
    37
    Now when you modify resolver code, Gradle automatically recompiles and restarts the server.
    38
    Build for Production
    39
    Create a production JAR that excludes development code:
    40
    ./gradlew build
    
    41
    The resulting artifact:
    42
  • Contains only src/main/kotlin code
  • Excludes viaduct-serve dependency
  • Excludes MicronautViaductFactory
  • Ready for deployment with a full Micronaut HTTP server
  • How Dependency Injection Works

    Resolver Instantiation Flow

    1. Query Execution: Viaduct needs a resolver instance
    2. DI Request: Calls tenantCodeInjector.getInstance(ResolverClass::class)
    3. Micronaut Resolution: MicronautTenantCodeInjector asks BeanContext for the bean
    4. Dependency Injection: Micronaut creates the resolver and injects its dependencies
    5. Caching: The instance is cached by Micronaut’s singleton scope

    Injecting Complex Dependencies

    You can inject any Micronaut bean into resolvers:
    @Singleton
    class UserRepository {
        suspend fun findById(id: String): User? { /* ... */ }
    }
    
    @Singleton
    class AuthService {
        fun getCurrentUserId(): String { /* ... */ }
    }
    
    @Resolver
    @Singleton
    class CurrentUserResolver(
        private val userRepository: UserRepository,
        private val authService: AuthService
    ) : QueryResolvers.CurrentUser() {
        override suspend fun resolve(ctx: Context): User? {
            val userId = authService.getCurrentUserId()
            return userRepository.findById(userId)
        }
    }
    

    Development Workflow

    Fast Iteration Cycle

    1. Start development server: ./gradlew --continuous serve
    2. Modify resolvers or schema
    3. Gradle auto-recompiles
    4. Refresh GraphiQL to test changes
    No manual restarts needed!

    Production Deployment

    1. Build production JAR: ./gradlew build
    2. Deploy to your server
    3. Run with Micronaut HTTP server:
    fun main(args: Array<String>) {
        Micronaut.run(Application::class.java, *args)
    }
    
    The Viaduct bean is available for injection into HTTP controllers.

    Why Use Micronaut with Viaduct?

    1. Dependency Injection: Resolvers can access databases, services, and external APIs
    2. Compile-Time DI: Micronaut performs DI at compile time (no reflection)
    3. Fast Startup: Minimal overhead compared to Spring
    4. Cloud-Native: Built for microservices and serverless
    5. Clean Separation: Development tools don’t bloat production artifacts

    Next Steps

    Build docs developers (and LLMs) love