Worked Examples
Worked Examples¶
These step-by-step examples will teach you how to build powerful GraphQL APIs with minimal code.
How It Works:¶
-
Write GraphQL schema with
@resolverdirectives -
Build your project - Viaduct generates resolver base classes
-
Implement resolvers - Extend generated classes with your logic
-
Get a working API - Type-safe, performant GraphQL server
Example Path¶
Follow these examples in order to learn Viaduct's resolver patterns. Kotlin examples come from core/tenant/tutorials/src/test/kotlin/viaduct/tenant. Java examples below come from the Java Star Wars application, which you can clone, run, and test. Both follow the same schema-first sequence.
Field Resolver Tutorial¶
Open the Kotlin feature-app test
Start here! Learn the most basic resolver type.
-
What you'll learn: How to create computed fields with custom logic
-
Key concepts: Field resolvers, Query type resolvers,
@Resolverannotation -
Example: A simple
foo: String!field that returns computed values -
Generated classes:
QueryResolvers.*
What you’ll learn: implement a computed field by extending its generated resolver base. Java returns CompletableFuture<T> and imports viaduct.java.api.annotations.Resolver.
package com.example.starwars.modules.filmography.characters.resolvers;
/** Demonstrates shorthand required-selection syntax. */
@Resolver(objectValueFragment = "name")
@Prototype
public final class CharacterDisplayNameResolver extends CharacterResolvers.DisplayName {
@Override
public CompletableFuture<String> resolve(Context context) {
return CompletableFuture.completedFuture(context.getObjectValue().getName());
}
}
The parent fields are declared in objectValueFragment before they are read. Continue with the field resolver walkthrough.
Node Resolver Tutorial¶
Open the Kotlin feature-app test
Learn the foundation of Viaduct's object system.
-
What you'll learn: How to create objects that can be refetched by ID
-
Key concepts: Node interface, GlobalID system, Node resolvers
-
Example: A
Footype that implementsNodeand can be resolved by ID -
Generated classes:
NodeResolvers.*, Global ID encoding/decoding -
Why it matters: Foundation for all object resolution in Viaduct
What you’ll learn: retrieve a node by its typed Global ID. Java obtains the internal key with context.getId().getInternalID() and returns a generated GRT asynchronously.
package com.example.starwars.modules.filmography.films.resolvers;
/** Node resolver for films. */
@Resolver
@Prototype
public final class FilmNodeResolver extends NodeResolvers.Film {
private final FilmsRepository filmsRepository;
@Inject
public FilmNodeResolver(FilmsRepository filmsRepository) {
this.filmsRepository = filmsRepository;
}
@Override
public CompletableFuture<Film> resolve(Context context) {
String filmId = context.getId().getInternalID();
com.example.starwars.modules.filmography.films.models.Film film =
filmsRepository.findFilmById(filmId);
if (film == null) {
throw new IllegalArgumentException("Film with ID " + filmId + " not found");
}
return CompletableFuture.completedFuture(new FilmBuilder(context).build(film));
}
}
The node resolver walkthrough includes client queries and missing-node behavior.
Simple Resolvers Tutorial¶
Open the Kotlin feature-app test
See how Field and Node resolvers work together.
-
What you'll learn: How different resolver types interact
-
Key concepts:
objectValueFragment, accessing parent data, resolver composition -
Example: A
Userwith computedfullNamefield usingfirstname+lastname -
Generated classes:
QueryResolvers.*,UserResolvers.*,NodeResolvers.* -
Advanced feature: Field resolvers accessing parent object data
What you’ll learn: compose a node with a related object field. The parent’s required fields are declared in the annotation; generated Java getters and builders keep the data flow typed.
package com.example.starwars.modules.filmography.characters.resolvers;
/** Batches character-to-homeworld references. */
@Resolver(objectValueFragment = "fragment _ on Character { id }")
@Prototype
public final class CharacterHomeworldResolver extends CharacterResolvers.Homeworld {
private final CharacterRepository characterRepository;
@Inject
public CharacterHomeworldResolver(CharacterRepository characterRepository) {
this.characterRepository = characterRepository;
}
@Override
public CompletableFuture<Map<Context, Planet>> batchResolve(List<Context> contexts) {
Map<Context, Planet> results = new IdentityHashMap<>();
for (Context context : contexts) {
Character character =
characterRepository.findById(context.getObjectValue().getId().getInternalID());
Planet planet =
character == null || character.homeworldId() == null
? null
: context.ref(
context.globalIDFor(Type.ofClass(Planet.class), character.homeworldId()));
results.put(context, planet);
}
return CompletableFuture.completedFuture(results);
}
}
Read Resolver Integration Patterns for how node, field, and batch resolvers compose.
Subqueries Tutorial¶
Open the Kotlin feature-app test
Execute subqueries against the Query and Mutation roots from inside a resolver.
-
What you'll learn: How to use
ctx.query()andctx.mutation()to issue imperative subqueries -
Key concepts:
ctx.query(),ctx.mutation(), subquery variable scoping, when to use subqueries vs declarative@Resolverfragments -
Example: A field resolver that fetches data from the Query root at runtime; a mutation resolver that calls another mutation via
ctx.mutation() -
Why it matters: Enables resolvers to fetch related data that isn't known at query planning time
What you’ll learn: execute a reusable, statically validated query operation from a resolver. Java composes the future with thenApply.
package com.example.starwars.modules.filmography.characters.queries;
/** Executes a statically declared Java query operation from a resolver. */
@Resolver
@Prototype
public final class CharacterSummaryByNameQueryResolver
extends QueryResolvers.CharacterSummaryByName {
@Override
public CompletableFuture<String> resolve(Context context) {
return context
.query(CharacterByNameOperation.INSTANCE, Map.of("name", context.getArguments().getName()))
.thenApply(
query -> {
viaduct.java.grts.Character character = query.getSearchCharacter();
if (character == null) {
return null;
}
String name = character.getName() == null ? "Unknown" : character.getName();
String birthYear =
character.getBirthYear() == null
? "Unknown birth year"
: character.getBirthYear();
return name + " (" + birthYear + ")";
});
}
}
GraphQL Operations shows the declaration and corresponding submutation. Java supports string selections and annotated documents; Kotlin’s typed SelectionSet overloads do not have a Java equivalent.
Key Concepts Across These Examples¶
The @resolver Directive¶
This tells Viaduct "generate a resolver for this". Different placements create different resolver types:
-
On object types:
type User @resolver→ CreatesNodeResolvers.User()(Node Resolver) -
On Query fields:
foo: String! @resolver→ CreatesQueryResolvers.Foo() -
On object fields:
fullName: String! @resolver→ CreatesUserResolvers.FullName()
Resolver Types¶
| Resolver Type | Purpose | Generated Class | When to Use |
|---|---|---|---|
| Node Resolver | Create/fetch objects by GlobalID | NodeResolvers.TypeName() |
Objects that implement Node interface |
| Query Field Resolver | Handle root query fields | QueryResolvers.FieldName() |
Top-level API entry points |
| Object Field Resolver | Compute derived fields | TypeResolvers.FieldName() |
Fields that need parent data or computation |
The GlobalID System¶
Viaduct's built-in global object identification:
-
Encodes: Object type + internal ID into a single string
-
Type-safe: Can't accidentally use wrong ID for wrong type
-
Utilities:
ctx.globalIDFor()to create,ctx.ref()to return a lazy node reference -
Standard: Follows Relay specification for global object identification
Field Resolver Data Access¶
Field resolvers can access parent object data:
-
Tells Viaduct exactly what parent data your resolver needs
-
Framework automatically fetches required fields
-
Available via generated getters such as
context.getObjectValue().getFirstnameOrThrow()(matching the field name declared in SDL)