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.objectValue.id)
    return FilmConnection.Builder(ctx)
      .fromList(allFilms) { film -> Film.Builder(ctx).title(film.title).build() }
      .build()
  }
}

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()
  }
}

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()
  }
}

Full Documentation

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

Resources