Pagination metadata tells API clients how to continue reading a list response.
The list items matter, but the metadata is what makes pagination usable, debuggable, and safe to evolve. Without clear metadata, clients guess whether there is another page, how large the page was, what cursor to send next, and whether total counts are reliable.
Quick answer
A good paginated REST response separates items from pagination metadata.
Example:
{
"items": [
{
"id": "evt_123",
"type": "invoice.paid"
}
],
"pageInfo": {
"limit": 50,
"hasNextPage": true,
"nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA3LTAxVDEyOjAwOjAwWiJ9"
}
}
For public APIs and changing datasets, prefer cursor metadata such as nextCursor and hasNextPage. Return exact totals only when they are cheap, correct, and useful.
Read Pagination Strategies for REST APIs before choosing offset, cursor, or keyset pagination.
Keep items and metadata separate
Do not overload the item array with pagination details.
Clear:
{
"items": [],
"pageInfo": {
"limit": 20,
"hasNextPage": false,
"nextCursor": null
}
}
Harder for clients:
[
{
"id": "item_1"
}
]
The second shape may be fine for tiny internal endpoints, but it leaves no room for cursors, limits, totals, warnings, or links without a breaking response change.
Common metadata fields
A cursor-based response often includes:
| Field | Meaning |
|---|---|
limit | Page size applied by the server |
hasNextPage | Whether another page is available |
nextCursor | Opaque token for the next page |
prevCursor | Optional token for backward pagination |
sort | Optional description of the active sort |
requestId | Optional correlation ID for debugging |
An offset-based response may include:
| Field | Meaning |
|---|---|
page | Current page number |
pageSize | Page size applied |
totalCount | Total matching items, if reliable |
totalPages | Derived from total count, if useful |
Do not include fields just because they are familiar. Include fields that clients can rely on.
Prefer opaque cursors
A cursor should usually be opaque.
Clients should treat this as a token:
GET /events?limit=50&cursor=eyJjcmVhdGVkQXQiOiIyMDI2LTA3LTAxIn0
They should not depend on the cursor being base64 JSON, a timestamp, or an ID. That gives the backend freedom to change cursor internals later.
Good cursor metadata:
{
"pageInfo": {
"hasNextPage": true,
"nextCursor": "opaque_token_here"
}
}
Avoid naming the cursor field after the current implementation, such as lastCreatedAt, unless you intend that implementation detail to become public contract.
Include the applied limit
Clients may request one page size, but the server should enforce a maximum.
Example:
GET /events?limit=1000
Response:
{
"items": [],
"pageInfo": {
"limit": 100,
"hasNextPage": true,
"nextCursor": "..."
}
}
Returning the applied limit makes behavior clear. It also helps SDKs, logs, and support teams understand why a response contains fewer items than requested.
Be careful with totalCount
totalCount is useful for admin tables, reports, and small filtered datasets.
It is risky for large or fast-changing datasets:
- Counts can be expensive.
- Counts can become stale while the client paginates.
- Counts can require different database plans than the item query.
- Counts can imply page numbers that cursor pagination does not support.
If totals are approximate, say so explicitly:
{
"pageInfo": {
"approximateTotalCount": 120000
}
}
Do not return a precise-looking count if it is not precise.
Stable sorting metadata
Pagination depends on stable ordering.
A good API should document the sort order:
{
"pageInfo": {
"sort": "createdAt:desc,id:desc",
"nextCursor": "..."
}
}
The unique tie-breaker matters. Sorting only by createdAt can be unstable when many records share the same timestamp. Adding id as a second sort key makes cursor and keyset pagination safer.
Read API Versioning Explained before changing default sorting for an existing endpoint.
Links vs cursors
Some APIs include pagination links:
{
"links": {
"next": "/events?limit=50&cursor=opaque_token_here"
}
}
Links are convenient for generic clients and documentation. Cursors are convenient for SDKs and application code.
You can include both if the API style benefits from it:
{
"pageInfo": {
"hasNextPage": true,
"nextCursor": "opaque_token_here"
},
"links": {
"next": "/events?limit=50&cursor=opaque_token_here"
}
}
Keep the contract consistent across list endpoints.
Filtering and cursor context
A cursor is only valid for the query context that created it.
If the client changes filters, search terms, tenant, user, or sort order, the old cursor should not be reused.
For opaque cursors, the backend can encode or verify context internally. For explicit keyset parameters, document which filters and sort fields must stay stable.
When a cursor is invalid, return a clear error:
{
"error": "invalid_cursor",
"message": "The pagination cursor is invalid or expired.",
"requestId": "req_123"
}
Read REST API Error Handling Best Practices for shared error shape design.
Empty pages and end states
Clients need predictable end behavior.
At the end of a cursor-paginated list:
{
"items": [],
"pageInfo": {
"limit": 50,
"hasNextPage": false,
"nextCursor": null
}
}
Do not omit pageInfo just because the page is empty. Empty responses are often where clients need metadata most.
OpenAPI documentation
Pagination metadata belongs in the API contract.
Document:
- Query parameters such as
limit,cursor,page, oroffset. - Maximum page size.
- Default sort.
- Response metadata shape.
- Cursor opacity.
- Total count behavior.
- Error response for invalid cursors.
Read OpenAPI Explained for Backend Developers and JSON Schema vs OpenAPI Schema when documenting reusable pagination schemas.
Common mistakes
The first mistake is returning only an array and later needing metadata. Adding a response wrapper later can break clients.
The second mistake is exposing cursor internals as if they were public fields.
The third mistake is promising totalCount on large endpoints without understanding the query cost.
The fourth mistake is using unstable sorting.
The fifth mistake is changing pagination metadata names across endpoints.
The sixth mistake is omitting metadata on empty pages.
Practical recommendation
For a first public REST API, use a consistent shape:
{
"items": [],
"pageInfo": {
"limit": 50,
"hasNextPage": false,
"nextCursor": null
}
}
Then add optional fields only when clients need them: prevCursor, sort, approximateTotalCount, or links.next.
Keep metadata stable, document it in OpenAPI, and treat pagination behavior as part of the API contract.
Related reading
Read Pagination Strategies for REST APIs for choosing offset, cursor, or keyset pagination, and REST API Design Checklist for reviewing list endpoints before launch.
For contract documentation, read OpenAPI Explained for Backend Developers, JSON Schema vs OpenAPI Schema, and API Versioning Explained. For the full sequence, browse the API Design Learning Path and the Topics map.