Viaduct generates an abstract base class for each Node type:
object NodeResolvers { abstract class User { open suspend fun resolve(ctx: Context): viaduct.api.grts.User = throw NotImplementedError() open suspend fun batchResolve(contexts: List<Context>): List<FieldValue<viaduct.api.grts.User>> = throw NotImplementedError() class Context: NodeExecutionContext<viaduct.api.grts.User> }}
import com.example.myapp.resolverbases.NodeResolversimport jakarta.inject.Injectimport viaduct.api.Resolverimport viaduct.api.grts.User@Resolverclass UserNodeResolver @Inject constructor( private val userService: UserServiceClient) : NodeResolvers.User() { override suspend fun resolve(ctx: Context): User { // Fetch data for a single user ID val userData = userService.getUser(ctx.id.internalID) return User.Builder(ctx) .firstName(userData.firstName) .lastName(userData.lastName) .email(userData.email) .build() }}
Node resolvers should not populate fields that have their own @resolver directive. In this example, we don’t set displayName because it has its own field resolver.
@Resolver( queryValueFragment = """ fragment _ on Query { currentUser { id role } } """)class PermissionCheckResolver : SomeResolvers.Field() { override suspend fun resolve(ctx: Context): Boolean { val currentUser = ctx.queryValue.getCurrentUser() val role = currentUser.getRole() return role == UserRole.ADMIN }}
Including nested fields (unless they have their own resolver)
But not the id field (it’s provided as input)
type User implements Node { id: ID! # Not included (input) firstName: String # Responsibility of node resolver lastName: String # Responsibility of node resolver email: String # Responsibility of node resolver displayName: String @resolver # NOT in node resolver's responsibility}
Object fields: The field and all nested fields without their own resolver
type User { profile: UserProfile @resolver # Field resolver's responsibility}type UserProfile { bio: String # Field resolver's responsibility website: String # Field resolver's responsibility avatar: Image @resolver # NOT in field resolver's responsibility}
override suspend fun resolve(ctx: Context): User { val userId = ctx.arguments.id.internalID val user = userService.getUser(userId) ?: throw IllegalArgumentException("User not found: $userId") return User.Builder(ctx) .name(user.name) .build()}
Implement batchResolve for node resolvers and field resolvers that fetch external data:
override suspend fun batchResolve(contexts: List<Context>): List<FieldValue<User>>
3
Return only your responsibility
Don’t set fields that have their own resolvers:
// Good - only sets fields in responsibility setUser.Builder(ctx) .firstName(data.firstName) .lastName(data.lastName) .build()// Bad - displayName has its own resolverUser.Builder(ctx) .firstName(data.firstName) .displayName("...") // Don't do this! .build()
4
Use node references for relationships
Use ctx.nodeFor() instead of inline building:
// Good - creates node referencePost.Builder(ctx) .author(ctx.nodeFor(authorId)) .build()// Bad - duplicates node resolver logicPost.Builder(ctx) .author( User.Builder(ctx) .firstName(...) .build() ) .build()