Overview
The Star Wars demo is a comprehensive GraphQL application that models the Star Wars universe with characters, films, species, planets, and starships. This tutorial demonstrates advanced Viaduct features including:- Multi-module architecture
- Field-level context and scoped fields
- Batch resolvers to prevent N+1 queries
- Global ID system with the Node interface
- Complex relationships between types
- Mock data repositories
What You’ll Learn
- How to structure a large-scale Viaduct application with multiple modules
- Implementing batch resolvers for efficient data loading
- Using scoped fields for field-level authorization
- Working with Viaduct’s Node interface and global IDs
- Building complex GraphQL queries with nested relationships
- Integrating Viaduct with Micronaut for dependency injection
Prerequisites
- Java JDK 21
JAVA_HOMEcorrectly set, orjavain your PATH- Understanding of GraphQL basics
Project Structure
{
"data": {
"allCharacters": [
{
"id": "Q2hhcmFjdGVyOjE=",
"name": "Luke Skywalker",
"homeworld": {
"name": "Tatooine"
}
},
// ... more characters
]
}
}
id field returns a base64-encoded global ID (e.g., Q2hhcmFjdGVyOjE= = Character:1)homeworld are resolved automaticallylimit parameter controls paginationSome fields are protected by scopes. For example,
Species.culturalNotes requires the extras scope.suspend fun graphql(
@Body request: Map<String, Any>,
@Header(SCOPES_HEADER) scopesHeader: String?,
@Header("security-access") securityAccess: String?
): HttpResponse<Map<String, Any?>> {
val scopes = parseScopes(scopesHeader)
val schemaId = determineSchemaId(scopes)
val result = viaduct.executeAsync(executionInput, schemaId).await()
// ...
}
query {
allCharacters(limit: 3) {
name
homeworld { name }
species { name }
filmCount
richSummary
}
}
{
"data": {
"allCharacters": [
{
"name": "Luke Skywalker",
"homeworld": { "name": "Tatooine" },
"species": { "name": "Human" },
"filmCount": 4,
"richSummary": "Luke Skywalker is a Human from Tatooine who appears in 4 films."
},
// ...
]
}
}
Fields like
filmCount and richSummary are computed by batch resolvers in CharacterResolvers.kt, SpeciesBatchResolver.kt, and FilmCountBatchResolver.kt.@BatchResolver
class FilmCountBatchResolver : CharacterResolvers.FilmCount() {
override suspend fun resolve(ctx: BatchContext): Map<Character, Int> {
// Efficiently fetch film counts for all characters at once
val filmCounts = filmRepository.getFilmCountsForCharacters(ctx.sources)
return ctx.sources.associateWith { character ->
filmCounts[character.id] ?: 0
}
}
}
Key Concepts Demonstrated
Multi-Module Architecture
The Star Wars app is organized into logical modules:- filmography: Characters and films
- universe: Planets, species, starships
- common: Shared data repositories and utilities
Batch Resolvers
Batch resolvers prevent N+1 query problems by processing multiple parent objects at once:Field-Level Scopes
Scoped fields allow fine-grained access control:Node Interface
Every major type implements theNode interface, providing:
- Global unique
idfield - Ability to query any object via
node(id: ID!) - Consistent identity across the schema
Next Steps
- Explore the source code in
demoapps/starwars/ - Review resolver implementations in the
modules/directories - Learn about Resolvers in the core documentation
- Study Batch Resolution for performance optimization
- Check out the Getting Started Guide for foundational concepts