Skip to content

Instantly share code, notes, and snippets.

@andyrichardson
Last active July 6, 2021 13:54
Show Gist options
  • Save andyrichardson/8e4248bf2cb91ea3a20bf6905e8d9d0c to your computer and use it in GitHub Desktop.
Save andyrichardson/8e4248bf2cb91ea3a20bf6905e8d9d0c to your computer and use it in GitHub Desktop.

Pagination

What is cursor based pagination

While there are many ways to paginate data, the most common pattern found nowadays is cursor-based.

What is a cursor

A cursor indicates how to find the position of an element within a collection.

Examples

DynamoDB cursor

Here's an example cursor returned from DynamoDB.

{ hash: "username", range: "01-01-2000" }

Optimizing cursors

It's not uncommon to base64 encode cursors to discourage tampering (as they are intended to resemble state).

eyJoYXNoIjoidXNlcm5hbWUiLCJyYW5nZSI6IjAxLTAxLTIwMDAifQ==

How does cursor pagination work

There's no single standard for cursor pagination. Here are a few examples:

Flows

While this isn't a full coverage of all use cases, here are some common flows for pagination.

Uni-directional pagination (single cursor)

Here's a generic example of how uni-directional pagination can take place.

Client requests a page and specifies the desired page size

GET /users?pageSize=3

Backend returns the data along with additional metadata.

{
  data: [
    { id: 1 },
    { id: 2 },
    { id: 3 },
  ],
  hasMore: true,
  endCursor: 'cursor-3'
}

Client makes pagination request again but also provides cursor

GET /users?pageSize=2&cursor=cursor-3

Note: Once the client gets the next page, it needs to find a way to aggregate said data into a single list

Uni-directional pagination (multi cursor)

Some more recent specs provide cursors for all nodes, not just the first/last. Here's why that might be necessary.

Client requests a page and specifies the desired page size

GET /users?pageSize=3

Backend returns the data along with additional metadata.

{
  data: [
    { cursor: 'cursor-1', data: { id: 1 } },
    { cursor: 'cursor-2', data: { id: 2 } },
    { cursor: 'cursor-3', data: { id: 3 } },
  ],
  hasMore: true,
  endCursor: 'cursor-3'
}

Client deletes the last element from the list

DELETE /users/3

Client fetches next page using cursor from (now) last node in the collection

GET /users?pageSize=3&cursor=cursor-2

Note: If single cursor pagination is used, some issues could be encountered from trying to use a cursor that references a non-existent node

Bi-directional pagination (single order)

It's becoming more common to see bi-directional pagination for large lists. With a single order approach, no assumptions are made about how data will be presented.

Client fetching from the front of the list

GET /users?first=2

Backend returning list in default order

{
  data: [
    { cursor: 'cursor-1', data: { id: 1 } },
    { cursor: 'cursor-2', data: { id: 2 } },
  ]
}

Client fetching from the back of the list

GET /users?last=2

Backend returning data (same order)

{
  data: [
    { cursor: 'cursor-4', data: { id: 4 } },
    { cursor: 'cursor-5', data: { id: 5 } },
  ]
}

Note: By having data in a consistent order, it becomes trivial to fetch from both-ends of a page (as you see with GitHub comments for example).

Sort order is left to the responsibility of the client.

Bi-directional pagination (ordered)

Some specs flip data ordering when fetching from the back of a list. This might save some presentational work at the cost of making it slightly more challenging to merge previously retrieved pages.

Client fetching from the front of the list

GET /users?size=2&order=forward

Backend returning list in default order

{
  data: [
    { cursor: 'cursor-1', data: { id: 1 } },
    { cursor: 'cursor-2', data: { id: 2 } },
  ]
}

Client fetching from the back of the list

GET /users?last=2

Backend returning data (different order)

{
  data: [
    { cursor: 'cursor-5', data: { id: 5 } },
    { cursor: 'cursor-4', data: { id: 4 } },
  ]
}

What do we need

First off - I think the most important thing here is that we don't make our own pagination spec. This is a solved problem, and while each solution has it's benefits/flaws, I think it would be naive to think we could come up with something better.

IMHO any spec which has the following should work:

  • Cursors for all nodes (multi cursor)
  • Bi-directional pagination (preferrably single order)

Slack

  • โŒ Single cursor
  • ๐ŸŸ  Bi-directional pagination (ordered)
  • Adoption: unknown

JSONAPI Cursor Pagination Profile

  • โœ… Cursors for all nodes
  • ๐ŸŸ  Bi-directional pagination (ordered)
  • โŒ Attatches cursors to nodes under a meta attribute (could lead to conflicts)
  • Adoption: unknown

Relay Cursor Connections Specification

  • โœ… Cursors for all nodes
  • โœ… Bi-directional pagination (single order)
  • ๐ŸŸ  wraps nodes in an edge (extra nesting)
  • ๐ŸŸ  designed with GraphQL in mind
  • Adoption: popular
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment