Pagination prevents large result sets from overwhelming clients and servers. Without pagination, servers take longer to query and build their response. In addition, clients must use considerable CPU and memory to process large responses. This can slow down or overwhelm applications.
13.1. Overview
There are three common styles typically encountered:
- Offset-based pagination
GET /customers?offset=40&limit=20- Simple but fragile at high scale (data shifts between pages, large offsets can be slow).
- Cursor-based pagination (recommended)
GET /customers?cursor=eyJpZCI6ImN1c18xMjM0NSJ9&limit=20- Cursor encodes position (often opaque, or last seen ID or timestamp); more stable under inserts/updates.
- Token-based / page-token pagination
GET /customers?pageToken=abc123&limit=50- Similar to cursor; token may come from underlying store/search engine.
Platform conventions you should standardize:
- Default limit and maximum allowed limit.
- Naming (cursor vs pageToken).
- Response envelope shape (e.g., items, nextCursor).
13.2. When to Use Pagination
Unless your data set is fixed and limited to a small set of resources, pagination should be used. Often, a threshold is established by the platform team, often around 100 or 200 resources, perhaps 25 or 50 if the resources tend to be large or are composed of nested resources.
13.3. When NOT to Use This Pattern
Don’t use pagination if the size is fixed and limited in size, as it will require multiple round trips unnecessarily.
13.4. Offset-Based Pagination OpenAPI Example
Below is a fragment of an OpenAPI specification that demonstrates offset-based pagination:
paths:
/customers:
get:
operationId: listCustomers
summary: List customers with offset-based pagination
description: |
Retrieves a paginated list of customers using **offset-based pagination**.
**How it works**:
- `offset`: Zero-based index of the first item to return
- `limit`: Maximum number of items to return
**Example**: `GET /customers?offset=40&limit=20` returns items 41-60
**Trade-offs**:
- ✅ Simple to implement and understand
- ✅ Allows random page access (jump to page N)
- ❌ Data shifts between pages if records are inserted/deleted
- ❌ Large offsets can be slow (database must skip N rows)
tags: [Customers]
parameters:
- name: offset
in: query
description: |
Zero-based index of the first item to return.
Example: `offset=20` skips the first 20 records.
required: false
schema:
type: integer
minimum: 0
default: 0
example: 20
- name: limit
in: query
description: |
Maximum number of items to return.
Platform default: 20. Maximum allowed: 100.
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
example: 10
responses:
'200':
description: Successful response with paginated customer list.
headers:
X-Total-Count:
description: Total number of customers matching filter criteria.
schema:
type: integer
example: 1543
X-Offset:
description: The offset used for this response.
schema:
type: integer
example: 20
X-Limit:
description: The limit used for this response.
schema:
type: integer
example: 10
content:
application/json:
schema:
type: array
description: |
Array of customers. No envelope wrapping.
Pagination metadata returned via headers.
items:
$ref: '#/components/schemas/Customer'
example:
- customerId: "550e8400-e29b-41d4-a716-446655440021"
name: "Alice Johnson"
email: "alice.johnson@example.com"
status: "ACTIVE"
createdDate: "2024-03-15T10:30:00Z"
- customerId: "550e8400-e29b-41d4-a716-446655440022"
name: "Bob Smith"
email: "bob.smith@example.com"
status: "ACTIVE"
createdDate: "2024-04-22T14:15:00Z"
'400':
description: Invalid request parameters.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
example:
type: "https://api.example.com/problems/invalid-parameter"
title: "Invalid Request Parameter"
status: 400
detail: "The 'offset' parameter must be a non-negative integer."
instance: "/customer-management/v1/customers"
13.5. Cursor-Based Pagination OpenAPI Example
Below is a fragment of an OpenAPI specification that demonstrates cursor-based pagination:
paths:
/customers:
get:
operationId: listCustomers
summary: List customers with cursor-based pagination
description: |
Retrieves a paginated list of customers using **cursor-based pagination**.
**How it works**:
- `cursor`: Opaque string encoding position in result set (omit for first page)
- `limit`: Maximum number of items to return
- Response includes `nextCursor` for fetching subsequent pages
**Example flow**:
1. `GET /customers?limit=20` → returns first 20 items + `nextCursor`
2. `GET /customers?cursor=eyJpZCI6...}&limit=20` → returns next 20 items
**Trade-offs**:
- ✅ Stable under concurrent inserts/updates
- ✅ Performs well at any position in dataset
- ✅ Cursor is opaque; implementation can change without breaking clients
- ❌ Cannot jump to arbitrary page (must traverse sequentially)
- ❌ Slightly more complex for clients to implement
tags: [Customers]
parameters:
- name: cursor
in: query
description: |
Opaque cursor encoding the position in the result set.
- Omit for the first page
- Use `nextCursor` from previous response for subsequent pages
- Cursor values should be treated as opaque strings
required: false
schema:
type: string
example: eyJjdXN0b21lcklkIjoiNTUwZTg0MDAtZTI5Yi00MWQ0LWE3MTYtNDQ2NjU1NDQwMDIwIn0=
- name: limit
in: query
description: |
Maximum number of items to return.
Platform default: 20. Maximum allowed: 100.
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
example: 20
responses:
'200':
description: Successful response with cursor-paginated customer list.
content:
application/json:
schema:
$ref: '#/components/schemas/CustomerCursorPagedCollection'
example:
items:
- customerId: "550e8400-e29b-41d4-a716-446655440001"
name: "Alice Johnson"
email: "alice.johnson@example.com"
status: "ACTIVE"
createdDate: "2024-03-15T10:30:00Z"
- customerId: "550e8400-e29b-41d4-a716-446655440002"
name: "Bob Smith"
email: "bob.smith@example.com"
status: "ACTIVE"
createdDate: "2024-04-22T14:15:00Z"
nextCursor: "eyJjdXN0b21lcklkIjoiNTUwZTg0MDAtZTI5Yi00MWQ0LWE3MTYtNDQ2NjU1NDQwMDAyIn0="
hasMore: true
'400':
description: Invalid request parameters.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
example:
type: "https://api.example.com/problems/invalid-cursor"
title: "Invalid Cursor"
status: 400
detail: "The provided cursor is malformed or expired."
instance: "/customer-management/v1/customers"
components:
schemas:
CustomerCursorPagedCollection:
type: object
description: |
Cursor-paginated collection response.
Contains items array and pagination metadata.
properties:
items:
type: array
description: Array of customer resources.
items:
$ref: '#/components/schemas/Customer'
nextCursor:
type: string
nullable: true
description: |
Opaque cursor for fetching the next page.
- Null or absent if no more results
- Pass as `cursor` parameter in next request
example: "eyJjdXN0b21lcklkIjoiNTUwZTg0MDAtZTI5Yi00MWQ0LWE3MTYtNDQ2NjU1NDQwMDAyIn0="
hasMore:
type: boolean
description: |
Indicates whether more results are available.
- `true`: More pages exist; use `nextCursor` to fetch
- `false`: This is the last page
example: true
required:
- items
- hasMore
13.6. Token-Based Pagination OpenAPI Example
Below is a fragment of an OpenAPI specification that demonstrates token-based pagination:
paths:
/customers:
get:
operationId: listCustomers
summary: List customers with page-token pagination
description: |
Retrieves a paginated list of customers using **page-token pagination**.
**How it works**:
- `pageToken`: Opaque token from previous response (omit for first page)
- `pageSize`: Number of items per page
- Response includes `nextPageToken` and optionally `previousPageToken`
**Difference from cursor-based**:
- Tokens may originate from underlying search engine or data store
- Often supports bidirectional navigation (next/previous)
- Token format/lifetime may be dictated by external system
**Example flow**:
1. `GET /customers?pageSize=25` → first page + `nextPageToken`
2. `GET /customers?pageToken=abc123&pageSize=25` → next page
**Trade-offs**:
- ✅ Natural fit for search engine backends (Elasticsearch, Solr)
- ✅ May support bidirectional navigation
- ✅ Stable pagination semantics
- ❌ Token expiration policies vary by backend
- ❌ Cannot jump to arbitrary page
tags: [Customers]
parameters:
- name: pageToken
in: query
description: |
Opaque page token from a previous response.
- Omit for the first page
- Use `nextPageToken` or `previousPageToken` from response
- Tokens may expire; handle 400 errors gracefully
required: false
schema:
type: string
example: dG9rZW5fMjAyNDEyMTVfY3VzdF8wMDIw
- name: pageSize
in: query
description: |
Number of items per page.
Platform default: 25. Maximum allowed: 100.
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 25
example: 25
responses:
'200':
description: Successful response with page-token pagination.
content:
application/json:
schema:
$ref: '#/components/schemas/CustomerTokenPagedCollection'
example:
items:
- customerId: "550e8400-e29b-41d4-a716-446655440001"
name: "Alice Johnson"
email: "alice.johnson@example.com"
status: "ACTIVE"
createdDate: "2024-03-15T10:30:00Z"
- customerId: "550e8400-e29b-41d4-a716-446655440002"
name: "Bob Smith"
email: "bob.smith@example.com"
status: "ACTIVE"
createdDate: "2024-04-22T14:15:00Z"
nextPageToken: "dG9rZW5fMjAyNDEyMTVfY3VzdF8wMDQw"
previousPageToken: "dG9rZW5fMjAyNDEyMTVfY3VzdF8wMDAw"
'400':
description: Invalid or expired page token.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
example:
type: "https://api.example.com/problems/invalid-page-token"
title: "Invalid Page Token"
status: 400
detail: "The provided pageToken is invalid or has expired. Please restart pagination from the first page."
instance: "/customer-management/v1/customers"
components:
schemas:
CustomerTokenPagedCollection:
type: object
description: |
Page-token paginated collection response.
Supports bidirectional navigation when available.
properties:
items:
type: array
description: Array of customer resources for this page.
items:
$ref: '#/components/schemas/Customer'
nextPageToken:
type: string
nullable: true
description: |
Token for fetching the next page.
- Null or absent if this is the last page
- Pass as `pageToken` parameter in next request
example: "dG9rZW5fMjAyNDEyMTVfY3VzdF8wMDQw"
previousPageToken:
type: string
nullable: true
description: |
Token for fetching the previous page.
- Null or absent if this is the first page
- Enables bidirectional navigation
example: "dG9rZW5fMjAyNDEyMTVfY3VzdF8wMDAw"
required:
- items