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 Name | Description |
---|---|
first | An IRI that refers to the furthest preceding resource in a series of resources. |
last | An IRI that refers to the furthest following resource in a series of resources. |
next | Indicates that the link's context is a part of a series, and that the next in the series is the link target. |
prev | Indicates 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 ofhttps://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\"" } } } } } }