Flow
Decisions

0014 — Pagination for list endpoints

Bracket query parameters and a meta.pagination block on every list.

Decided. List endpoints use bracket query parameters and a meta.pagination block in the response.

Alternatives

  • Flat limit / offset query params with counts in the payload root (template today). Works, but diverges from the bracket-parameter and meta.pagination contract front ends already use.
  • Opaque cursor tokens. Better for very large or high-churn lists, but a different client contract. Reserved for endpoints that declare paginate: cursor and a reason: in the entity spec.
  • Page params without meta. Forces every client to re-derive pageCount and couples list shape to each resource name (widgets, accounts, …) instead of a universal data array.

Why

Bracket parameters (pagination[page], pagination[pageSize]) and a data + meta.pagination response are already in use across existing front ends and internal tools. Adopting the same shape here avoids a translation layer and gives every list endpoint one predictable contract.

Request

Two modes. Never mix them on one endpoint.

Page mode (default)

ParameterTypeDefaultDescription
pagination[page]integer1Page number (1-based)
pagination[pageSize]integer25Items per page

Offset mode (opt-in per list endpoint)

ParameterTypeDefaultDescription
pagination[start]integer0Zero-based index of first item
pagination[limit]integer25Items to return

Each list endpoint declares a maxPageSize (or maxLimit for offset mode) in the entity spec. Default page size is 25; cap at 100 unless a domain needs otherwise.

Implementation maps page mode to LIMIT / OFFSET in SQL:

offset = (page - 1) * pageSize

Response

List payloads use a data array and meta.pagination object. Create and get put the record in data as an object. Services that wrap responses in the template {header, payload} envelope put this object inside payload:

{
  "header": { "timestamp": "…", "status": 200, "request_id": "…" },
  "payload": {
    "data": [ … ],
    "meta": {
      "pagination": {
        "page": 1,
        "pageSize": 25,
        "pageCount": 4,
        "total": 100
      }
    }
  }
}

Page mode always includes page, pageSize, pageCount, and total. pageCount is ceil(total / pageSize) with a minimum of 1 when total > 0, and 0 when total == 0.

Offset mode:

"meta": {
  "pagination": {
    "start": 0,
    "limit": 25,
    "total": 100
  }
}

total is the count of rows matching the query filters, ignoring pagination. A separate COUNT(*) query (or an equivalent) is always included unless an endpoint explicitly opts out.

Consequences

  • The generator emits bracket pagination query params and the data / meta.pagination response struct for every paginate: page (or paginate: offset) list in the entity spec.
  • Template GET /v1/domain1 and GET /v1/domain2 use this page-mode contract.
  • Cursor pagination remains available but is exceptional — needs paginate: cursor and a named reason, not the default for ordinary CRUD lists.

Canonical source: docs/decisions/0014-pagination.md.

On this page