Skip to content

Pagination

Viaduct implements the Relay Connection specification, providing cursor-based pagination with built-in builder utilities.

Schema

Define your connection and edge types using the @connection and @edge directives:

type FilmConnection @connection {
  edges: [FilmEdge!]!
  pageInfo: PageInfo!
}

type FilmEdge @edge {
  node: Film!
  cursor: String!
}

Fields that return a connection accept standard Relay pagination arguments:

type Character implements Node {
  id: ID!
  name: String!
  films(first: Int, after: String): FilmConnection!
}

Building Connection Responses

Connection builders provide three strategies depending on your backend:

fromList — in-memory list

Use when your resolver has the full dataset. Viaduct handles slicing and cursor encoding automatically:

@Resolver
class CharacterFilmsResolver : CharacterResolvers.Films() {
  override suspend fun resolve(ctx: Context): FilmConnection {
    val allFilms = filmRepository.getFilmsForCharacter(ctx.getObjectValue().getIdOrThrow().internalID)
    return FilmConnection.Builder(ctx)
      .fromList(allFilms) { film -> Film.Builder(ctx).title(film.title).build() }
      .build()
  }
}
package com.example.starwars.modules.filmography.characters.queries;

/** Resolves Relay-style character pagination. */
@Resolver
@Prototype
public final class AllCharactersConnectionQueryResolver
    extends QueryResolvers.AllCharactersConnection {
  private final CharacterRepository characterRepository;

  @Inject
  public AllCharactersConnectionQueryResolver(CharacterRepository characterRepository) {
    this.characterRepository = characterRepository;
  }

  @Override
  public CompletableFuture<CharactersConnection> resolve(Context context) {
    return CompletableFuture.completedFuture(
        CharactersConnection.builder(context)
            .fromList(
                characterRepository.findAll(),
                character -> new CharacterBuilder(context).build(character))
            .build());
  }
}

View full file on GitHub

The Java demo uses allCharactersConnection for this strategy. Generated builders use Type.builder(context) and a Java mapping function; the context supplies the pagination arguments.

fromSlice — offset/limit backend

Use when your backend accepts offset and limit. Over-fetch by one to detect hasNextPage:

@Resolver
class CharacterFilmsResolver : CharacterResolvers.Films() {
  override suspend fun resolve(ctx: Context): FilmConnection {
    val (offset, limit) = ctx.arguments.toOffsetLimit()
    val fetched = filmService.getFilms(offset, limit + 1)
    return FilmConnection.Builder(ctx)
      .fromSlice(fetched.take(limit), hasNextPage = fetched.size > limit) { film ->
        Film.Builder(ctx).title(film.title).build()
      }
      .build()
  }
}
package com.example.starwars.modules.universe.starships.queries;

/** Resolves forward pagination for starships using a limit-plus-one slice. */
@Resolver
@Prototype
public final class AllStarshipsConnectionQueryResolver
    extends QueryResolvers.AllStarshipsConnection {
  private final StarshipsRepository starshipsRepository;

  @Inject
  public AllStarshipsConnectionQueryResolver(StarshipsRepository starshipsRepository) {
    this.starshipsRepository = starshipsRepository;
  }

  @Override
  public CompletableFuture<StarshipsConnection> resolve(Context context) {
    OffsetLimit offsetLimit = context.getArguments().toOffsetLimit();
    List<Starship> allStarships = starshipsRepository.findAll();
    int fromIndex = Math.min(offsetLimit.offset(), allStarships.size());
    int toIndex = Math.min(allStarships.size(), fromIndex + offsetLimit.limit() + 1);
    List<Starship> slicePlusOne = allStarships.subList(fromIndex, toIndex);
    boolean hasNextPage = slicePlusOne.size() > offsetLimit.limit();

    return CompletableFuture.completedFuture(
        StarshipsConnection.builder(context)
            .fromSlice(
                slicePlusOne, hasNextPage, starship -> new StarshipBuilder(context).build(starship))
            .build());
  }
}

View full file on GitHub

context.getArguments().toOffsetLimit() returns an OffsetLimit. Pass the slice and hasNextPage to the Java builder. Over-fetch by one to detect the next page.

fromEdges — native cursors or edge metadata

Use when your backend returns native cursors, or when your edge type carries extra fields beyond node and cursor:

@Resolver
class CharacterFilmsResolver : CharacterResolvers.Films() {
  override suspend fun resolve(ctx: Context): FilmConnection {
    val response = filmService.getFilms(cursor = ctx.arguments.after, limit = ctx.arguments.first ?: 20)
    return FilmConnection.Builder(ctx)
      .fromEdges(
        edges = response.films.map { film ->
          FilmEdge.Builder(ctx)
            .node(Film.Builder(ctx).title(film.title).build())
            .cursor(film.cursor)
            .build()
        },
        hasNextPage = response.hasMore
      )
      .build()
  }
}
package com.example.starwars.modules.universe.species.queries;

/** Resolves multidirectional species pagination using manually constructed edges. */
@Resolver
@Prototype
public final class AllSpeciesConnectionQueryResolver extends QueryResolvers.AllSpeciesConnection {
  private final SpeciesRepository speciesRepository;

  @Inject
  public AllSpeciesConnectionQueryResolver(SpeciesRepository speciesRepository) {
    this.speciesRepository = speciesRepository;
  }

  @Override
  public CompletableFuture<SpeciesConnection> resolve(Context context) {
    OffsetLimit offsetLimit =
        context.getArguments().requiresTotalCountForOffsetLimit()
            ? context
                .getArguments()
                .toOffsetLimit(speciesRepository.count(), ConnectionArguments.DEFAULT_PAGE_SIZE)
            : context.getArguments().toOffsetLimit();

    List<Species> slicePlusOne =
        speciesRepository.findSome(offsetLimit.limit() + 1, offsetLimit.offset());
    boolean hasNextPage = slicePlusOne.size() > offsetLimit.limit();
    boolean hasPreviousPage = offsetLimit.offset() > 0;
    int edgeCount = Math.min(offsetLimit.limit(), slicePlusOne.size());
    List<SpeciesEdge> edges = new ArrayList<>(edgeCount);
    for (int index = 0; index < edgeCount; index++) {
      edges.add(
          SpeciesEdge.builder(context)
              .node(new SpeciesBuilder(context).build(slicePlusOne.get(index)))
              .cursor(OffsetCursor.fromOffset(offsetLimit.offset() + index).getValue())
              .build());
    }

    return CompletableFuture.completedFuture(
        SpeciesConnection.builder(context).fromEdges(edges, hasNextPage, hasPreviousPage).build());
  }
}

View full file on GitHub

The Java demo constructs SpeciesEdge objects and calls fromEdges(edges, hasNextPage, hasPreviousPage). For backward pagination its argument conversion uses the total item count.

Full Documentation

For the complete pagination reference — argument types, toOffsetLimit(), cursor encoding, and PageInfo handling — see the Pagination developer docs.

Resources