API design

API Pagination Best Practices for REST APIs

Choose a paging model, make response metadata predictable, and protect collection endpoints from unbounded requests.

Published: 19 September 2026 ยท APISAST

Updated: 19 September 2026

Why pagination belongs in the API contract

API pagination best practices start with a bounded collection. Without a documented page size, a list endpoint may return too much data, slow down clients, and make scraping cheaper. A limit in the OpenAPI contract also tells SDK authors and consumers how to traverse results. APISAST flags collection operations that do not declare a recognised paging parameter; the finding is a design prompt, not proof that the server returns unlimited records.

Choose the approach that matches the data and the client journey. Then document the accepted parameters, defaults, upper bounds, ordering, and a reliable way to ask for the next page.

Offset vs cursor pagination

ApproachGood fitTrade-off
Offset: ?limit=20&offset=40Small, mostly stable datasets and direct page navigationRows inserted between requests can shift page boundaries.
Cursor: ?limit=20&after=tokenChanging feeds and large datasetsClients follow a next token instead of jumping to page 42.

For cursor paging, define a stable sort key and a tie-breaker such as an immutable ID. Treat the cursor as opaque: clients should pass it back unchanged. Sign or validate cursors if tampering could reveal data across tenants. A cursor does not replace an authorization check on each request.

Document limits and the next page

A useful OpenAPI definition describes limit as an integer with a default and maximum, and after as an optional string. The response should include an array of items and a nullable nextCursor. Say whether filters and sort order must remain unchanged while paging. If a cursor expires, return a clear client error and tell callers how to restart.

GET /orders?limit=20&after=eyJpZCI6...
{
  "items": [{ "id": "ord_1042" }],
  "nextCursor": "eyJpZCI6..."
}

Set a sensible maximum even when clients ask for a larger page. Return only fields the caller is allowed to see, and check tenant ownership before applying filters. Pair pagination with rate limiting for repeated collection requests.

Choose a stable ordering rule

Pagination only works when the server defines an order. Sorting solely by a non-unique timestamp can repeat or skip records when several items share the same value. Add a unique tie-breaker such as an immutable order ID and specify whether results are newest-first or oldest-first. For a cursor, encode the last seen sort values on the server and resume strictly after that position. A client should never have to parse the token to understand the next request.

Offset pagination is easier when a user needs to jump to an arbitrary page, but changing rows can shift subsequent offsets. A cursor usually gives more stable traversal of a busy feed. Neither approach gives a consistent snapshot automatically: newly inserted records may still appear or disappear during a long walk. If a use case requires a fixed export, create an explicit snapshot or export job and describe how long its token remains valid. State the consistency guarantee in documentation so consumers do not mistake paging for a transaction.

Write an OpenAPI contract clients can follow

Define the paging parameters on the collection operation, including type, default, upper bound, and examples. A response schema should expose the list and the continuation value in a stable shape. Document what a null continuation means and whether an empty page can still have a next token. If the API also supports filtering, state that the caller must keep the same filters and sort order for the entire traversal. This removes guesswork for SDK authors and makes contract tests straightforward.

For a cursor-based endpoint, a short excerpt might define an optional after query parameter and a limit integer constrained to 1โ€“100. The 200 response then documents items and nextCursor; a 400 response explains malformed or expired cursors. The number 100 is an example policy, not an APISAST rule. Choose the actual maximum after measuring query cost and response size in your service. The scanner detects recognised pagination parameter names in a contract; it does not verify every value of the bound.

Protect cursors and collection data

Treat continuation tokens as inputs from an untrusted caller. Validate their format, expiry, and association with the current user or tenant where needed. An opaque cursor can still disclose information if it is merely base64-encoded JSON. Do not include secrets or personal data in a token sent to clients; sign or encrypt state when tampering or disclosure matters. A changed tenant or filter should invalidate the token rather than silently resuming in the wrong dataset.

Authorization must run on every page. Check object ownership before results enter a page and ensure totals, cursors, and error messages do not reveal records outside the caller's scope. Apply a limit to page size and a separate request quota to the caller. These controls serve different purposes: page bounds cap the work of one request, while rate limiting caps repeated requests. Test a user who changes object IDs, filters, or tokens between pages; a clean OpenAPI result alone cannot rule out data exposure.

Measure the experience at real scale

Try the first page, a deep page, and a walk through the full collection with realistic data volumes. Measure database time, response bytes, and the percentage of clients that abandon traversal. Large offsets may force a database to skip many rows; a cursor can help when an index matches the sort keys. Still, a poorly chosen cursor query can be slow. Use query plans and load tests to choose the approach rather than assuming one is always faster.

Build integration tests around empty results, maximum page size, invalid tokens, duplicate sort values, and concurrent inserts. Check that a client can stop when nextCursor is null and recover from a 400 without entering a retry loop. Keep an example response in the OpenAPI file and compare it with the live service in CI. This gives contract consumers a usable example and exposes drift that a static scan cannot observe.

If the API exposes a total count, decide whether the number is exact, approximate, or omitted for large collections. An exact count may be expensive and can reveal information across access boundaries. Measure it separately from the page query and make the guarantee explicit. Explain what happens when a filter returns no records, whether the next cursor is absent, and how clients should restart after an expired token. These details are small in the contract but prevent common client bugs.

Test the contract and the service

  • Scan the OpenAPI file for missing paging parameters with the OpenAPI security scanner.
  • Test empty pages, invalid cursors, maximum limits, changing datasets, and authorization across tenants.
  • Measure query time and response size at realistic page depths; an OpenAPI scan cannot measure database cost.

Run the API security scanner after updating the contract, then confirm the implementation follows the documented limits.

More API security guides