Pagination
Hypermedia pagination

Hypermedia pagination

Navigate to related pages of data from links included with responses.

Providing links to navigate paginated data allows an API consumers to "browse" the API by following the link target, much like an end user browses the web. This is a more RESTful and hypermedia-driven approach, which leads to a more discoverable API.

An example of a response containing pagination links might look like:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "data": [
    {
      "title": "First result"
    },
    {
      "title": "Second result"
    }
  ],
  "links": {
    "first": "https://api.example.com/records?page=1",
    "last": "https://api.example.com/records?page=213",
    "next": "https://api.example.com/records?page=5",
    "prev": "https://api.example.com/records?page=3"
  }
}

Link relations

The RFC 8288 standard defines semantics for links. The keys of the links object are defined according to IANA Link Relations database.

Relation NameDescription
firstAn IRI that refers to the furthest preceding resource in a series of resources.
lastAn IRI that refers to the furthest following resource in a series of resources.
nextIndicates that the link's context is a part of a series, and that the next in the series is the link target.
prevIndicates that the link's context is a part of a series, and that the previous in the series is the link target.

The Link header

RFC 8288 also defines a way to serialize links into HTTP headers. APIs can adopt this standard provide an industry-recognized, consistent way to implement pagination.

The same response will look like:

HTTP/1.1 200 OK
Content-Type: application/json
Link: <https://api.example.com/records?page=1>; rel="first", <https://api.example.com/records?page=213>; rel="last", <https://api.example.com/records?page=5>; rel="next", <https://api.example.com/records?page=3>; rel="prev"

{
  "data": [
    {
      "title": "First result"
    },
    {
      "title": "Second result"
    }
  ]
}

This approach has the advantage of clearly seperating the resource's representation, and metadata about the resource. It also follows an industry-recognized format for both semantics and serialization format.

The following code parses the link header into a map structure, keyed by the link relation type:

function linksFromHeader(header) {
  if (!header) {
    return null;
  }

  const links = {};
  const parts = header.split(",");

  for (const part of parts) {
    const section = part.split(";");
    if (section.length !== 2) {
      continue;
    }

    const urlMatch = section[0].match(/<(.+)>/);
    const relMatch = section[1].match(/rel="(.+)"/);

    if (urlMatch && relMatch) {
      const url = urlMatch[1];
      const rel = relMatch[1];
      links[rel] = url;
    }
  }

  return links;
}

Note that the Link header is not exclusive to pagination, and supports other link relation types.

Pros

  • More RESTful design with hypermedia links
  • Follows web standards (RFC 8288)
  • Improved discoverabilty of related pages
  • Backwards compatible when treating links as opaque URLs

Cons

  • No random access. Links do not provide random access to arbitrary pages of data. This constrains the user experiences to sequential navigation, for example starting from the first page and navigating forward one page at a time.
  • The Link header may be more difficult to consume for some clients. This can be mitigated by providing client libraries.
  • Less visiblity of response data. Many developers do not inspect response headers, at least not at first, when writing and debugging code. This can APIs more difficult to learn for developers who are accustomed to only looking at JSON response bodies.
  • Increasted development and testing cost. Since the links are full URLs that include the hostname, extra care must be taken during implementation that the returned links are correct for each environment. For example, the same response might return links starting with http://staging-api.example.com instead of https://api.example.com when running in a staging environment.
  • Prone to reverse engineering. While links do promise backwards compatibilty. The internal structure can easily be inspected and reverse engineered. This may result in clients expecting certain query parameters to work indefinitely, even where they do not form part of the official API contract. Since it is impossible to know whether a client is following a link or construcing a URL, you won't detect clients relying on unsupported query parameters until it is too late.
  • Limited metadata. The response only provides references to other resources, i.e. other pages of data. Responses do return metadata about the result set as a whole, such as the total number of pages, total number of results, or page size.
  • Larger response size due to entire URL included for next, prev, first and last links.

Usage

An API consumer can iterate through all pages using the following code:

async function fetchPaginatedData(url) {
  let results = [];

  while (url) {
    const response = await fetch(url);
    const data = await response.json();
    results = results.concat(data.items);

    // Get the Link header from the response
    const linkHeader = response.headers.get("Link");
    const links = linksFromHeader(linkHeader);

    // Check if there is a "next" link
    if (links && links.next) {
      url = links.next;
    } else {
      url = null;
    }
  }

  return results;
}

// Example usage
const apiUrl = "https://api.example.com/items";
fetchPaginatedData(apiUrl)
  .then((results) => {
    console.log(results);
  })
  .catch((error) => {
    console.error(error);
  });

Implementation notes

Implementing link-based pagination is not mutually exclusive with other methods. When implemented using the Link header, link-based pagination can easily be implementation on top of other methods.

However, all pagination methods form part of your APIs contract and thus need to be considered for backwards compatiblity.

OpenAPI implementation

{
  "openapi": "3.1.0",
  "info": {
    "title": "Hypermedia Pagination Example API",
    "version": "1.0.0"
  },
  "paths": {
    "/records": {
      "get": {
        "summary": "list all records",
        "responses": {
          "200": {
            "headers": {
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "Link": {
        "description": "Contains web links related to the resource, formatted according to [RFC 8288](https://datatracker.ietf.org/doc/html/rfc8288).\n\nWhen included in paginated responses, contains `first`, `last`, `next` and `prev` links.",
        "required": false,
        "schema": {
          "type": "string",
          "pattern": "^((.*); rel=\"(.*)\")(, ((.*); rel=\"(.*)\")*)$"
        },
        "examples": {
          "Pagination": {
            "summary": "A Link header value containing pagination links.",
            "value": "<https://api.example.com/records?page=1>; rel=\"first\", <https://api.example.com/records?page=213>; rel=\"last\", <https://api.example.com/records?page=5>; rel=\"next\", <https://api.example.com/records?page=3>; rel=\"prev\""
          }
        }
      }
    }
  }
}

Alternative design patterns

Offset-based pagination

Access paginated data via an offset into the total dataset.

Read more

Cursor-based pagination

Access paginated data via an opaque token.

Read more

Was this page helpful?

Made by Criteria.