All Go backend list endpoints return a consistent paginated response envelope. Two modes are supported: offset (page-numbered) and cursor (token-based).
Response Shape
{
"items": [],
"pagination": { ... }
}
The top-level items array always contains the page of data. The pagination object contains navigation metadata.
Offset Mode
Use for static datasets where total count is known and users may jump to arbitrary pages (e.g. audit logs, reports).
{
"items": [{ "id": "...", "...": "..." }],
"pagination": {
"mode": "offset",
"limit": 20,
"hasNext": true,
"hasPrev": false,
"page": 1,
"totalPages": 24,
"totalRecords": 480
}
}
Cursor Mode
Use for frequently-updated lists where consistent ordering matters (e.g. activity feeds, inventory, transactions).
{
"items": [{ "id": "...", "...": "..." }],
"pagination": {
"mode": "cursor",
"limit": 20,
"hasNext": true,
"hasPrev": false,
"nextCursor": "eyJpZCI6ImFiYzEyMyJ9",
"prevCursor": null
}
}
Field Reference
| Field | Type | Modes | Description |
|---|
mode | "offset" | "cursor" | both | Which pagination strategy was used |
limit | number | both | Page size (max 100, default 20) |
hasNext | boolean | both | Whether a next page exists |
hasPrev | boolean | both | Whether a previous page exists |
page | number | offset | Current page number (1-indexed) |
totalPages | number | offset (cursor opt.) | Total number of pages |
totalRecords | number | offset (cursor opt.) | Total record count |
nextCursor | string | cursor | Opaque token to fetch the next page |
prevCursor | string | cursor | Opaque token to fetch the previous page |
hasNext and hasPrev are always present in both modes — use them to enable/disable navigation controls without branching on mode.
hasNext / hasPrev Semantics
| Mode | hasNext | hasPrev |
|---|
| offset | page < totalPages | page > 1 |
| cursor | nextCursor != null | prevCursor != null |
Both fields are computed server-side at response time.
Switching Modes
Send the X-Pagination-Type request header to select the mode. Defaults to offset when the header is absent.
X-Pagination-Type: cursor
Cursor Mode with Total Count
By default cursor responses omit totalPages and totalRecords to avoid an extra COUNT query. Pass ?include_total=true to opt in:
GET /api/v1/leave/requests?limit=20&include_total=true
X-Pagination-Type: cursor
include_total=true triggers an additional database COUNT query on every request. Use it only when the total count is needed by the UI — for example, showing “480 records” alongside cursor navigation.
Request Parameters
| Parameter | Description |
|---|
limit | Page size, 1–100 (default: 20) |
page | Page number for offset mode (default: 1) |
offset | Byte offset alternative to page |
cursor | Cursor token for cursor mode (from previous response) |
include_total | true to include totalPages/totalRecords in cursor mode |
Handler Usage (Go)
paginationParams := pagination.ParseParams(c)
// Offset response
meta := pagination.NewOffsetMeta(paginationParams.Limit, paginationParams.Offset, total)
return c.JSON(http.StatusOK, pagination.Wrap(items, meta))
// Cursor response
meta := pagination.NewCursorMeta(paginationParams.Limit, nextCursor, prevCursor)
return c.JSON(http.StatusOK, pagination.Wrap(items, meta))
// Cursor response with optional total count
if paginationParams.IncludeTotal {
meta = pagination.NewCursorMetaWithTotal(paginationParams.Limit, nextCursor, prevCursor, total)
}
return c.JSON(http.StatusOK, pagination.Wrap(items, meta))
The pagination.Wrap function always produces { "items": ..., "pagination": ... } — handlers must not construct the envelope manually.
Limit Cap
The maximum allowed limit is 100. Requests with limit > 100 fall back to the default of 20.