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/offsetquery params with counts in the payload root (template today). Works, but diverges from the bracket-parameter andmeta.paginationcontract 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: cursorand areason:in the entity spec. - Page params without
meta. Forces every client to re-derivepageCountand couples list shape to each resource name (widgets,accounts, …) instead of a universaldataarray.
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)
| Parameter | Type | Default | Description |
|---|---|---|---|
pagination[page] | integer | 1 | Page number (1-based) |
pagination[pageSize] | integer | 25 | Items per page |
Offset mode (opt-in per list endpoint)
| Parameter | Type | Default | Description |
|---|---|---|---|
pagination[start] | integer | 0 | Zero-based index of first item |
pagination[limit] | integer | 25 | Items 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) * pageSizeResponse
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.paginationresponse struct for everypaginate: page(orpaginate: offset) list in the entity spec. - Template
GET /v1/domain1andGET /v1/domain2use this page-mode contract. - Cursor pagination remains available but is exceptional — needs
paginate: cursorand a named reason, not the default for ordinary CRUD lists.
Canonical source: docs/decisions/0014-pagination.md.