Name: Pagination
Status: approved
Created: 2022-02-05
Updated: 2023-05-24

Pagination (#158)

APIs often need to provide collections of data. Collections can be arbitrarily sized, and usualy grow over time, increasing lookup time as well as the size of the responses. Therefore, it is important that collections be paginated.

There are two popular pagination styles: offset-limit and cursor pagination. Refer to this article for definitions and comparison.

Guidance

All collection endpoints MUST be paginated from the outset, since adding pagination is a backward-incompatible change.

Collections in Kong APIs generally fall into one of the following categories:

  1. customer-managed collections like services, versions, runtime groups
  2. system-generated, unbounded collections like audit log entries
  3. second-order human-created entities like consumers, developers, and applications

APIs at Kong should prefer cursor pagination. In particular, system-generated, unbounded collections must use cursor pagination. Customer-managed and second-order collections like consumers should use cursor pagination, since the size of these collections will vary widely across Kong customers. An outline of offset-limit pagination is offered here for exceptional cases where cursor pagination doesn't meet API client needs.

Cursor

APIs that implement cursor pagination should follow the guidelines below, which are inspired by the JSON-API Pagination profile.

Sorting Requirement

Pagination only applies to an ordered list of results. This order must not change between requests unless the underlying data changes, to ensure that results don’t arbitrarily move between pages.

If the client’s paginated request includes a ?sort query parameter that only partially orders the results, the server MUST apply additional sorting constraints — consistent with the client-requested ones — to produce a unique ordering, if it wishes to support pagination of that data.

Similarly, when the collection being paginated has no natural or client-requested order, the server MUST assign an order if it wishes to support pagination.

The server MAY reject pagination requests if the client has requested that the results be sorted in a way that server cannot efficiently paginate. In that case, the server MUST reject the request with a 400 response.

Cursor Values

Cursors should be opaque, so that the implementation can change without breaking clients who may write business logic against cursor values.

Guidance

It is RECOMMENDED for servers to calculate cursor values using the following encoding strategy:

cursor = base64url_encode(xor_cipher(raw_cursor_value))

The contents of raw_cursor_value is an implementation detail left up to the API server. An example could be a stringified JSON object with properties that provide a pointer to the recordset, for example:

{
"id": "d8d16afe-082b-4851-a008-a6c0fd928519",
"created_at": "2023-05-18T00:00:00Z",
}

Query Parameters

Endpoints that support cursor pagination accept a page query param. page is an object with the following optional properties:

size - the number of results that the client would like to see in the response. If not provided, the server must choose a default page size.

after and before - the pages after or before a given cursor. Both are optional. They are also mutually exclusive, meaning that only one of them can be used in a single API request. Both parameters cannot be present simultaneously (see Range Pagination).

See https://jsonapi.org/profiles/ethanresnick/cursor-pagination/#auto-id-query-parameters for more details about query parameters.

Range Pagination

A range pagination request is defined by an API request that includes both page[after] and page[before] query parameters.

Kong APIs do not support range pagination requests and must return a 400 response indicating that range pagination is not supported when one is received.

Response Body

The response body of a paginated endpoint should include the following attributes:

data - the subset of records being returned

meta.page - an object that includes page info such as

  • size - the requested page size
  • next - URI to the next page (may be null)
  • previous - URI to the previous page (may be null)
  • first - URI to the first page (optional)
  • last - URI to the last page (optional)

first and last properties are optional properties. Which means if the API does support those fields, first and last SHOULD always be defined.

For clarity, the size response property is always a copy of the value supplied via query parameter. This still applies when:

  • size does not equal the length of the data array

It is RECOMMENDED that servers include first and last links when these are inexpensive to compute.

Servers MUST include previous and next links for each instance of paginated data in a response when data for those links is available.

Servers SHOULD set these links to null when it can inexpensively determine that the current response is for the first or last page respectively.

However, if the server can’t easily determine whether there are prior results (when computing the previous link) or subsequent results (when computing the next link), it MAY use a URI in these links that returns an empty array as its paginated data.

URIs MUST include all query parameters that were sent in the API request with the exception of:

  • page[next]
  • page[previous]
  • page[first]
  • page[last]

since these parameters are calculated per-request for pagination by the server.

Note that the order of the query parameters in the API request and the corresponding response links can vary.

Examples

Suppose we have a collection of critters. Note: the meta attribute of each record is optional.

[
{ "name": "cats", "id": "uuid-1", "meta": {"page": {"cursor" : "dXVpZC0x"} } },
{ "name": "dogs", "id": "uuid-5", "meta": {"page": {"cursor" : "dXVpZC01"} } },
{ "name": "ants", "id": "uuid-7", "meta": {"page": {"cursor" : "dXVpZC03"} } },
{ "name": "emus", "id": "uuid-8", "meta": {"page": {"cursor" : "dXVpZC04"} } },
{ "name": "bats", "id": "uuid-9", "meta": {"page": {"cursor" : "dXVpZC05"} } }
]

... with a default page size of 2 and default sort by id. A call to /critters would yield

{
"data": [
{ "name": "cats", "id": "uuid-1" },
{ "name": "dogs", "id": "uuid-5" }
],
"meta": {
"page": {
"size": 2,
"previous": null,
"next": "/critters?page[after]=dXVpZC01"
}
}
}

To change the page size, pass the size in a page parameter: /critters?page[size]=4:

{
"data": [
{ "name": "cats", "id": "uuid-1" },
{ "name": "dogs", "id": "uuid-5" }
{ "name": "ants", "id": "uuid-7" },
{ "name": "emus", "id": "uuid-8" }
],
"meta": {
"page": {
"size": 4,
"previous": null,
"next": "/critters?page[after]=dXVpZC04&page[size]=4"
}
}
}

To fetch the next page, use the value in page.next, which is /critters?page[after]=dXVpZC04&page[size]=4:

{
"data": [
{ "name": "bats", "id": "uuid-9" },
],
"meta": {
"page": {
"size": 4,
"previous": "/critters?page[before]=dXVpZC05&page[size]=4",
"next": null
}
}
}

Query parameters provided in the request must be persisted in the URI links returned in the response. A call to /critters?foo=1&sort=id will result in:

{
"data": [
{ "name": "cats", "id": "uuid-1" },
{ "name": "dogs", "id": "uuid-5" }
],
"meta": {
"page": {
"size": 2,
"previous": null,
"next": "/critters?page[after]=dXVpZC01&foo=1&sort=id"
}
}
}

Limit-Offset

Collections that have low cardinality may implement limit-offset pagination. When they do, the API should support the following properties in the page object of the request, and echo them back in the page meta of the response:

  • size - the requested page size
  • number - the current page number (optional, the default number is 1)

For clarity, these response properties are always a copy of the values supplied via query parameter. This still applies when:

  • size does not equal the length of the data array
  • number is greater than the total number of available pages

Examples

Given the same dataset as above, to fetch the second page of results, use /critters?page[number]=2:

{
"data": [
{ "name": "ants", "id": "uuid-7" },
{ "name": "emus", "id": "uuid-8" },
],
"meta": {
"page": {
"number": 2,
"size": 2,
"total": 5
}
}
}

A request that extends beyond the end of the dataset, such as /critters?page[number]=5&page[size]=10:

{
"data": [
],
"meta": {
"page": {
"number": 5,
"size": 10,
"total": 5
}
}
}

Total and Estimated Total

Regardless of the pagination strategy selected, the response may include either a total or estimated_total in the response.

References