Skip to content

Pagination and Relay Connections — Complete Guide

DodaTech Updated 2026-06-28 3 min read

In this tutorial, you will learn about Pagination and Relay Connections. We cover key concepts, practical examples, and best practices to help you master this topic.

The Relay Connection specification provides a standardized pattern for cursor-based pagination in Graphql. Connections wrap lists with edges containing cursors and a node, plus pageInfo for navigation metadata.

What You'll Learn

  • Connection, Edge, and PageInfo type definitions
  • Cursor-based pagination with forward and backward navigation
  • first/after and last/before pagination arguments
  • Implementing connection resolvers
  • Total count and other metadata

Why It Matters

Cursor-based pagination provides consistent results when data changes between pages. The Relay pattern is the industry standard for GraphQL pagination, supported by Apollo Client and Relay.

Real-World Use

GitHub uses Relay connections for all list fields: RepositoryConnection, IssueConnection, PullRequestConnection. Shopify's Storefront API uses connections for product listings with cursor navigation.

flowchart LR
    Query[Query] --> Connection[BookConnection]
    Connection --> Edges[Edges]
    Connection --> PageInfo[PageInfo]
    Edges --> Edge1[Edge 1]
    Edges --> Edge2[Edge 2]
    Edge1 --> Cursor[Cursor: base64]
    Edge1 --> Node[Node: Book]
    PageInfo --> HasNext[hasNextPage]
    PageInfo --> HasPrev[hasPreviousPage]
    PageInfo --> Start[startCursor]
    PageInfo --> End[endCursor]

Teacher Mindset

Think of connections as paginated lists. Each item is wrapped in an edge with a cursor (opaque pointer). Clients use cursors to request the next or previous page. The pageInfo object tells clients about navigation possibilities.

Code Examples

# Example 1: Connection types
type BookConnection {
  edges: [BookEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type BookEdge {
  cursor: String!
  node: Book!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}
# Example 2: Paginated query with arguments
type Query {
  books(
    first: Int
    after: String
    last: Int
    before: String
  ): BookConnection!
}
// Example 3: Connection resolver implementation
const resolvers = {
  Query: {
    books: async (_, { first = 10, after, last, before }) => {
      const limit = first || last || 10;
      const cursor = after || before;

      let query = db.books.orderBy('id');
      if (cursor) {
        const decodedId = Buffer.from(cursor, 'base64').toString();
        const op = after ? '>' : '<';
        query = query.where('id', op, decodedId);
      }

      const items = await query.limit(limit + 1).fetch();
      const hasMore = items.length > limit;
      if (hasMore) items.pop();

      const edges = items.map(item => ({
        cursor: Buffer.from(String(item.id)).toString('base64'),
        node: item
      }));

      return {
        edges,
        pageInfo: {
          hasNextPage: hasMore && !before,
          hasPreviousPage: !!before || (!!after && false),
          startCursor: edges[0]?.cursor,
          endCursor: edges[edges.length - 1]?.cursor
        },
        totalCount: await db.books.count()
      };
    }
  }
};

Common Mistakes

  • Using offset-based pagination (skip/limit) instead of cursor-based
  • Making cursors human-readable instead of opaque strings
  • Not returning totalCount for UI display
  • Mixing forward (first/after) and backward (last/before) in the same query
  • Off-by-one errors in the hasNextPage calculation

Practice

  1. Define BookConnection, BookEdge, and PageInfo types.
  2. Implement forward pagination with first and after arguments.
  3. Add backward pagination with last and before arguments.
  4. Include totalCount in the connection response.
  5. Challenge: Implement a complex connection with sorting and filtering.

FAQ

Why use cursor-based pagination instead of offset?

Cursor-based pagination stays consistent when items are added or removed. Offset pagination can skip or duplicate items.

What should the cursor value be?

Cursors should be opaque strings. Base64-encode a database identifier or a combination of sort value and ID.

How do I implement hasNextPage?

Fetch limit+1 items. If the result has limit+1 items, hasNextPage is true. Discard the extra item.

Can connections include aggregated data?

Yes. Add fields like totalCount and averageRating to the connection type for aggregated metadata.

Is the Relay spec required for pagination?

No, but it is the most widely adopted standard. It ensures consistency across your API and compatibility with client tools.

Mini Project

Convert your library books list query to use Relay connections. Implement forward pagination with cursor encoding. Add totalCount to the connection. Test pagination with a set of 25 books.

What's Next

Next, you will learn about Apollo Federation for building a distributed GraphQL architecture.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro