Documenting Pagination in API Contracts
When a client reads a large API collection, the server returns the records in portions called pages. The pagination contract tells the…
When a REST API returns a large collection, the server can split the results into smaller responses called pages. The server sends one page in the response body and uses the HTTP Link header to point to related pages. A client follows the URL labeled next to continue, or prev to move backward when that link is available.
RFC 8288 defines how the header represents links. The API provider defines what each pagination URL does and what its query values mean. Those values might include a page number or a cursor, a provider-defined value that identifies where to continue. In practice, the client reads the link and requests its URL rather than building the next URL itself.
Link header points to another pageA Link header contains one or more links. Each link pairs a target URL, enclosed in angle brackets, with parameters that describe the link. The rel parameter says how the target URL relates to the current response, such as whether it leads to the next or previous page.
HTTP/1.1 200 OK
Content-Type: application/json
Link: ; rel="next",
; rel="prev"
{"items":[{"id":1},{"id":2},{"id":3}]}
The schematic response contains three items and two navigation links: the URL paired with rel="next" leads to the next page, and the URL paired with rel="prev" leads to the previous one. The cursor value is part of the URL, and its format and meaning belong to the API’s contract.
The IANA link relation registry lists standard names for link relationships. It defines next as the next resource in a series and lists previous as a synonym for prev. Providers can choose which form to return, so clients should follow the relation names documented for the API they use. GitHub documents prev and next; Shopify’s REST Admin API documentation uses previous and next.
The Link header also supports links unrelated to pagination. RFC 8288 defines a general linking mechanism, while each API decides whether to use it for page navigation and which links to include.
A client starts with the collection URL, processes the response body, then looks for a link whose relation is next. If one exists, the client sends a request to that exact target and repeats the same steps.
url = initial_collection_url
while url exists:
response = GET url
process(response.body)
links = parse_Link_header(response.headers["Link"])
url = target whose relation includes "next"
When the API’s documented contract uses the absence of a next link to mark the end, the client stops when it finds no such link, as GitHub documents this pattern. GitHub also notes that it can omit the entire Link header when an endpoint does not support pagination or when all results fit on one page. A missing header alone therefore does not tell the client which situation applies.
A client should use code that parses the Link header’s syntax. The header can contain multiple links and parameters, with values written in different forms. Splitting the header on commas or semicolons without accounting for that syntax can misread valid links. Follow the URLs and relation names the API returns instead of assuming a particular query parameter or constructing a URL from a page number.
A cursor identifies a continuation point according to the API provider’s rules. An API may put it in a URL as a query parameter, then return that URL in the Link header. The client generally needs to pass the URL through unchanged; it does not need to decode the cursor or calculate a replacement.
For example, Shopify’s REST Admin API documentation describes a page_info value that must be reused exactly as it appears in the link URL. Shopify also documents those cursor-bearing URLs as temporary. These rules apply to Shopify’s API and should not be assumed for another provider. Shopify labels its REST Admin API as legacy and recommends that new apps and integrations use its GraphQL Admin API.
The Link header tells the client which URLs to request to move through pages. It does not specify how the API divides the collection. A target URL might use a page number, a cursor, or another provider-defined parameter. GitHub documents links that use parameters such as page and before or after; Shopify’s REST Admin API uses page_info. Check the endpoint’s documentation for parameter behavior and page-size limits. For example, GitHub documents a maximum per_page value of 100 for most endpoints, while Shopify’s REST Admin API guide documents a different limit for its pagination parameters.
The link standard does not define a universal cursor format, cursor lifetime, or snapshot guarantee. A snapshot guarantee would mean that every page request reads from one frozen version of the collection. Without an explicit provider promise, clients should not assume that inserts, deletions, or updates during traversal leave the pages unchanged.
Providers also decide which navigation links to return. An API might supply first, last, prev, or next, but the standard does not require every response to contain all of them. GitHub, for example, documents that it omits the previous link on the first page and may omit the last link when it cannot calculate it.
Treat a supplied link as input from the server, especially when the client attaches credentials to requests. RFC 8288 notes that link content is not inherently secure or integrity-protected. A client operating in a sensitive environment should validate link destinations and check how its HTTP client handles credentials when the destination changes.
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com