Backend (Go) - Pagination API 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

FieldTypeModesDescription
mode"offset" | "cursor"bothWhich pagination strategy was used
limitnumberbothPage size (max 100, default 20)
hasNextbooleanbothWhether a next page exists
hasPrevbooleanbothWhether a previous page exists
pagenumberoffsetCurrent page number (1-indexed)
totalPagesnumberoffset (cursor opt.)Total number of pages
totalRecordsnumberoffset (cursor opt.)Total record count
nextCursorstringcursorOpaque token to fetch the next page
prevCursorstringcursorOpaque 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

ModehasNexthasPrev
offsetpage < totalPagespage > 1
cursornextCursor != nullprevCursor != 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

ParameterDescription
limitPage size, 1–100 (default: 20)
pagePage number for offset mode (default: 1)
offsetByte offset alternative to page
cursorCursor token for cursor mode (from previous response)
include_totaltrue 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.