REST API Pagination Example: Response Format, Implementation and Best Practices
REST API Pagination Example & Best Practices
REST API Design

REST API Pagination Example: Response Format, Implementation and Best Practices

A practical REST API pagination guide with request and response examples for page, offset and cursor models. The goal is not simply to add page=1: pagination must stay deterministic under inserts, enforce server-side limits, preserve filtering and sorting, and expose a continuation mechanism clients can use safely.

Implementation guideAPI Example
Primary patternCursor for large/active sets
Simple patternPage / offset
Critical ruleStable deterministic sort
Security ruleEnforce maximum page size

What Is the Best REST API Pagination Pattern?

A strong REST API pagination example starts with a stable ordering and a bounded page size. Use page/offset pagination when direct page navigation matters, and cursor/keyset pagination when datasets are large or frequently changing.

For simple, mostly static lists

Use page + page_size or offset + limit. It is easy to understand and supports direct jumps, but large offsets can become expensive and concurrent inserts can shift results.

For large or frequently changing lists

Use an opaque cursor based on a stable sort key. Cursor/keyset pagination usually gives more consistent traversal and avoids scanning past very large offsets.

A good pagination contract gives the client a bounded slice of data plus enough information to request the next slice. The most important design decision is not the parameter spelling—it is whether the ordering stays stable while data changes.

Rule of thumb: choose one primary model per collection endpoint. Do not make clients guess whether page, offset and cursor can be mixed in the same request.

REST API Pagination Response Example

For a customer list sorted by creation time and ID, a cursor-based endpoint can look like this:

GET /v1/customers?limit=50&after=eyJjcmVhdGVkX2F0IjoiMjAyNi0xMC0wNlQwOTozMDowMFoiLCJpZCI6IjEwNTIifQ

The response can keep collection data and pagination metadata separate:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "data": [
    {"id":"cus_1053","name":"Northwind"},
    {"id":"cus_1054","name":"Contoso"}
  ],
  "pagination": {
    "limit": 50,
    "has_more": true,
    "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0xMC0wNlQwOTozNDowMFoiLCJpZCI6IjEwNTQifQ"
  }
}

The cursor should be treated as opaque by the client. It may encode a keyset internally, but clients should store and replay it rather than parse or modify it.

Page-Based REST API Pagination Response Example

For smaller or relatively stable datasets, a page-based contract can be simpler for consumers that need direct page navigation:

GET /v1/customers?page=3&page_size=25

HTTP/1.1 200 OK
Content-Type: application/json

{
  "data": [ ...25 customers... ],
  "pagination": {
    "page": 3,
    "page_size": 25,
    "has_more": true,
    "next_page": 4
  }
}

Return total_items or total_pages only when the product needs an exact count and the backend can calculate it affordably. Cursor APIs usually do not need a total count to traverse a collection.

Page, Offset and Cursor Pagination Compared

PatternExampleBest fitTrade-off
Page?page=3&page_size=25Human-facing pages and stable datasetsEasy navigation, but data shifts can produce skips/duplicates.
Offset?offset=50&limit=25Simple SQL-backed collectionsLarge offsets can be costly; concurrent writes can move rows.
Cursor / keyset?after=opaque_cursor&limit=25Large or rapidly changing datasetsFast and stable, but arbitrary page jumps are harder.

Cursor pagination should use a deterministic ordering. If created_at is not unique, add a unique tiebreaker such as id. The query then advances beyond the last tuple rather than skipping an arbitrary number of rows.

What Should a Paginated REST API Response Include?

  • The current items. Keep the resource representation independent from pagination mechanics.
  • The effective limit/page size. Return the actual server-applied value if the client requested something larger.
  • A continuation signal. Use next_cursor, a next link, or an equivalent field instead of forcing clients to calculate state.
  • has_more or next-link semantics. This makes the end condition explicit.
  • Total counts only when useful and affordable. Exact counts can be expensive on large or filtered datasets and are not required for cursor traversal.
  • Consistent filters and sort. A cursor must be bound to the same logical query so it cannot silently resume under different ordering rules.

Web Linking (RFC 8288) can also be used for relations such as next and prev. JSON metadata is often easier for API clients, while link relations are useful when you want navigation encoded at the HTTP/hypermedia layer.

Link Header Pagination Example

APIs that use HTTP link relations can advertise navigation separately from the JSON representation. RFC 8288 defines the Web Linking model:

Link: </v1/customers?after=CURSOR123&limit=50>; rel="next"

You can expose a next_cursor in JSON, a rel="next" link, or both. Pick a stable contract instead of making clients reconstruct continuation URLs from undocumented rules.

When pagination is combined with user-selected filters and sorting, keep the continuation state bound to the same query. See REST API filtering and sorting examples.

REST API Pagination Implementation Example

A keyset query can advance on the same tuple used for sorting:

SELECT id, name, created_at
FROM customers
WHERE (created_at, id) > (:cursor_created_at, :cursor_id)
ORDER BY created_at ASC, id ASC
LIMIT :limit_plus_one;

Fetch one extra row. If the query requested 51 rows for a page size of 50 and receives 51, return the first 50 and set has_more=true. Build the next cursor from the last returned item, not from the extra row.

For filtered endpoints, include or cryptographically bind the relevant filter/sort state to the cursor. Otherwise a client could reuse a cursor from status=active with status=disabled and receive inconsistent results.

Pagination Security and Abuse Controls

  • Cap page size. A client-supplied limit=1000000 must not turn a normal collection endpoint into a memory, CPU or database amplification path.
  • Validate numeric bounds. Reject negative offsets, non-integer limits, malformed cursors and unsupported sort fields.
  • Do not expose sensitive database keys unintentionally. Opaque cursors are useful when the internal continuation state should not become part of the public contract.
  • Authorize every page. Pagination must never snapshot authorization from page one and reuse it blindly for later requests.
  • Rate-limit expensive traversal. Automated clients can walk every page; protect bulk enumeration according to the sensitivity of the collection.

Pagination is part of endpoint security, not only UX. See Ammune’s REST API endpoint security best practices and API rate limiting guidance for the surrounding controls.

Common REST API Pagination Mistakes

  • Returning an unbounded collection and asking clients to paginate locally.
  • Sorting only by a non-unique field, which makes row order unstable.
  • Using large offsets on tables where the database must scan or discard millions of rows.
  • Changing the filter or sort semantics between pages without invalidating the cursor.
  • Always computing an exact total count even when it dominates query cost.
  • Returning cursor internals as a client contract and later discovering they cannot be changed.

How to Test Pagination Correctly

  1. Request the first page with the minimum, normal and maximum allowed page size.
  2. Insert and delete records between page requests and verify cursor traversal does not skip or duplicate items unexpectedly.
  3. Test equal sort values to confirm the unique tiebreaker produces deterministic order.
  4. Replay malformed, expired and filter-mismatched cursors.
  5. Verify authorization on later pages with a user whose access changes during traversal.
  6. Measure database execution time for early and deep pages under representative data volume.

How to Choose a REST API Page Size

A page-size default should be large enough to avoid excessive round trips but small enough to keep response time, memory, serialization cost and client processing predictable. Start with real object sizes and latency targets rather than copying a fashionable number. Fifty tiny records can be cheaper than ten records with nested expansions or large text fields.

Expose a client-controlled limit only inside a bounded range. If the default is 50 and the hard maximum is 200, a request for limit=5000 can either be rejected or clamped according to the documented contract. Returning the effective limit in pagination metadata helps clients understand what the server actually applied.

Collection profileTypical design directionReason
Small admin tablePage/offset with modest page sizeDirect page jumps may be valuable and dataset is bounded.
High-volume event streamCursor/keyset with smaller pagesStable continuation and deep traversal performance matter.
Large objects / expansionsLower maximum limitResponse bytes and serialization dominate item count.
Public enumerable directoryStrict limit + rate controlPrevents one request from becoming a bulk extraction primitive.

Pagination Consistency, Cursors and Changing Data

No pagination mechanism creates a magical snapshot unless the backend actually provides snapshot semantics. With a live dataset, records can be inserted, updated or deleted between requests. The goal is to define behavior that is understandable and avoids avoidable duplicates or gaps.

Keyset pagination anchored to the last returned sort tuple is usually resilient to new rows inserted “before” or “after” the cursor, but updates to the sort key can still move records. If the business requires a consistent export, create an explicit snapshot/export job instead of promising snapshot behavior through an ordinary collection endpoint.

# Example cursor state before signing/encoding
{
  "sort": ["created_at", "id"],
  "last": ["2026-10-06T09:34:00Z", "cus_1054"],
  "filter_hash": "sha256:...",
  "expires_at": "2026-10-06T10:34:00Z"
}

Expiry is optional but useful when the server cannot guarantee that old continuation state remains valid forever. If a cursor expires, return a clear error that tells the client to restart traversal rather than silently interpreting the value under new rules.

How Ammune Fits

Pagination can become a security and business-logic concern when clients enumerate large datasets, manipulate query parameters, or walk sensitive object collections at machine speed. Ammune provides runtime API visibility and behavior analysis that can help distinguish expected pagination from unusual enumeration, scraping, or data-exfiltration patterns.

That runtime context complements server-side pagination limits and authorization; it does not replace them.

Production Implementation Checklist

  • Choose one pagination model for each collection and document it.
  • Define a deterministic default sort with a unique tiebreaker.
  • Set a default page size and hard maximum.
  • Return an explicit next cursor/link instead of making clients invent continuation state.
  • Validate and protect cursor integrity where the cursor contains server state.
  • Keep filters and sort consistent across cursor requests.
  • Test inserts, deletes and equal sort values between pages.
  • Measure deep-page database cost with production-sized data.
  • Authorize every page request independently.
  • Monitor automated traversal and bulk enumeration behavior.

Frequently Asked Questions

What is the best pagination method for a REST API?

Cursor/keyset pagination is usually the strongest choice for large or frequently changing datasets. Page or offset pagination remains useful for simpler and more static collections.

What should a REST API pagination response contain?

Return the items plus clear pagination metadata such as the effective limit, a next cursor or link, and an explicit indication of whether more data is available. Include total counts only when clients need them and the count is affordable.

Is cursor pagination better than offset pagination?

Cursor pagination is usually more stable under concurrent inserts and performs better at deep positions. Offset pagination is simpler and supports direct jumps, so it can still be appropriate for small or relatively static datasets.

Should a pagination cursor be encoded or encrypted?

It should at least be opaque to clients. Encoding alone is not integrity protection. If clients must not be able to alter cursor state, sign or otherwise protect the cursor against tampering.

How large should an API page be?

There is no universal number. Pick a default and hard maximum based on response size, latency, database cost and client needs, then enforce the limit server-side.

Primary References

Protect High-Volume API Collection Access

Good pagination controls query cost. Ammune adds runtime visibility into how clients actually traverse, enumerate and use API collections.

© Ammune.ai — API security guidance for modern application environments.