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