TidyURLs
Go to Tools
BLOG
The TidyURLsJournal

Practical guides, tutorials, and insights about URLs, SEO, web development, and online marketing. Learn, troubleshoot, and get the most of the web.

Back to the Journal
TidyURLs Journal

How to Build Reliable REST API Pagination Links

Practical insights, clear explanations, and useful ideas for understanding the web.

October 28, 20267 min read5 topics
How to Build Reliable REST API Pagination Links
Explore the details
#REST API#API design#Pagination#Backend development#HTTP
October 28, 20267 min read

Pagination links let API clients move through a large result set without guessing how the server represents its state. This tutorial compares page-number, offset, and cursor pagination, shows request and response examples, and explains what clients should do when a next link is missing or a cursor fails.

Key Takeaways

  • Choose pagination based on the data and consistency the API needs: page numbers are familiar, offsets support direct positioning, and cursors work well for sequential traversal of changing datasets.
  • Return a complete next-page URL when possible. Clients can follow it without rebuilding query parameters or knowing the pagination algorithm.
  • Define what a missing next link, invalid cursor, and expired cursor mean in the API contract.
  • Keep a stable sort order, encode URL values correctly, and treat cursors as opaque state—not as authorization.

Choose a pagination model

Each model communicates position differently. No model guarantees a consistent snapshot if records are changing; the API must decide whether consistency across requests matters.

ModelExample requestStrengthsTrade-offs
Page number/v1/orders?page=3&page_size=25Easy for users to understand; supports jumping to a numbered page.Inserts or deletes before the current page can shift records, so clients may see duplicates or miss items. Large page numbers can also require expensive work, depending on storage and query strategy.
Offset/v1/orders?offset=50&limit=25Directly describes how many records to skip; useful for simple datasets and interfaces that need arbitrary positions.A changing dataset can shift the offset between requests. Skipping a large number of rows may be costly in some databases.
Cursor/v1/orders?limit=25&cursor=eyJpZCI6MTIzLCJzbmFwIjoiYTIifQSupports sequential traversal without requiring clients to calculate a row number. A cursor based on a stable ordering can behave better than offsets when rows are inserted.Usually does not support jumping to an arbitrary page. Cursor format, expiry, and consistency behavior need a clear contract.

A cursor does not automatically provide a snapshot. For example, a cursor that marks the last seen (created_at, id) can avoid ambiguity when timestamps repeat, but the API still needs to define how updates and deletions affect later results. If clients must traverse an unchanging result set, consider binding the cursor to a snapshot or query version and documenting its lifetime.

Make the next link the source of truth

A response can include navigation links in its body, in an HTTP Link header, or both. A body field is straightforward for JSON clients; a Link header uses the Web Linking convention for expressing relationships such as next. Pick a format and document it consistently.

For example, a JSON response might look like this:

{
  "data": [
    { "id": "ord_101", "total": 42.50 },
    { "id": "ord_102", "total": 18.00 }
  ],
  "links": {
    "self": "https://api.example.com/v1/orders?status=open&limit=2",
    "next": "https://api.example.com/v1/orders?status=open&limit=2&cursor=eyJpZCI6MTIzLCJzbmFwIjoiYTIifQ"
  ]
}

The next URL carries forward the status filter and page size as well as the cursor. The client should follow the supplied URL rather than extracting the cursor and constructing a new request. That lets the server change its internal cursor scheme without requiring a client update.

If using a Link header, a response could include:

Link: <https://api.example.com/v1/orders?status=open&limit=2&cursor=eyJpZCI6MTIzLCJzbmFwIjoiYTIifQ>; rel="next"

The examples use illustrative domains and values. The header shown follows the common Web Linking form; if adopting it, check the formal syntax and any client-library requirements for the API you support.

A page-number API can return links too:

{
  "data": [{ "id": "ord_101" }],
  "links": {
    "next": "https://api.example.com/v1/orders?page=4&page_size=25"
  }
}

Links are useful even when the API uses page or offset parameters. Clients do not have to infer whether the next request should increment a page, add a limit to an offset, or preserve filters in a particular order.

Build and follow URLs safely

Keep query parameters as data and let a URL API perform the encoding. Avoid joining raw strings: values may contain spaces, &, +, or other characters that change how a query is parsed.

For a JavaScript client that needs to add a filter to a known endpoint:

const url = new URL("https://api.example.com/v1/orders");
url.searchParams.set("status", "needs review & approval");
url.searchParams.set("limit", "25");

console.log(url.toString());

The URL API encodes the filter value so its ampersand is not interpreted as a new query parameter. When the API returns a complete next link, use that link directly instead of rebuilding it with URLSearchParams.

A simple client loop can follow body links until the server indicates there is no next page:

async function* pages(firstUrl, fetchPage) {
  let nextUrl = firstUrl;

  while (nextUrl) {
    const page = await fetchPage(nextUrl);
    yield page.data;
    nextUrl = page.links?.next ?? null;
  }
}

This is illustrative: fetchPage must be provided by the application and should handle HTTP errors, authentication, and response parsing. A client that follows links automatically should also enforce its own destination policy. For example, if credentials are attached to requests, do not blindly send them to a link on an unexpected origin. Validate that links point to an allowed API origin, or use a client library that applies a documented safe policy.

Define edge-case behavior

No next link

Treat a missing next link as the end only if the API contract says that absent navigation links indicate there are no more results. Do not infer completion from a short page: an API may return fewer than the requested limit while still having more results. If the response format instead always includes next, document whether it is null, an empty value, or omitted at the end.

Invalid or expired cursor

A cursor can be malformed, tampered with, expired, or invalid for a different filter or sort order. Return a clear client error and a stable machine-readable error code. If cursors expire, document how clients should recover—for example, by restarting from the first page. Do not silently reinterpret an invalid cursor as the beginning of the result set; that can cause confusing duplicate processing.

A cursor is not a substitute for authorization. Check permissions on every request, even when the cursor is opaque or signed.

Changing records and ordering

Always define an order that breaks ties. Sorting only by a timestamp may be ambiguous when several records have the same timestamp; adding a unique identifier gives the server a stable tie-breaker. Explain whether records added during traversal can appear, whether updates can move a record, and whether a snapshot is available.

Filters and page-size changes

A cursor generally belongs to a particular query and order. If clients change filters or sorting while reusing a cursor, reject the combination or define explicitly how it behaves. Decide whether clients may change the page size between requests; if supported, make sure the cursor remains meaningful.

Common Mistakes

  • Returning only a cursor when clients need a URL. A client may then have to know the endpoint, preserve filters, and reconstruct encoding rules. Prefer returning the complete next URL when practical.
  • Assuming offsets are stable. Inserts and deletes can shift positions between requests. Use an appropriate stable ordering, or choose cursor or snapshot semantics when the use case requires them.
  • Treating every absent next link the same way. Specify whether it means end-of-results; otherwise clients cannot distinguish completion from a malformed response.
  • Putting sensitive data in a cursor. URLs can appear in logs, browser history, monitoring systems, and referrer data. Use opaque, non-sensitive tokens and avoid putting credentials or private record contents in them.
  • Following arbitrary links with credentials. Automatically following pagination URLs is convenient, but clients should not leak authorization headers to untrusted hosts.
  • Building query strings by concatenation. Use URL libraries or correct percent-encoding so values do not accidentally become additional parameters.

Best Practices

  1. Choose page, offset, or cursor semantics to match the product and data store; document the trade-offs.
  2. Return complete navigation links and preserve the request’s filters and sort order.
  3. Use a deterministic ordering with a unique tie-breaker.
  4. Specify end-of-results, invalid-cursor, expiry, and recovery behavior.
  5. Set a maximum page size and define whether clients may request a smaller or larger size on later pages.
  6. Test traversal with inserts, deletions, repeated sort values, empty results, malformed cursors, and encoded query values.
  7. For clients that follow links automatically, restrict credential-bearing requests to trusted destinations.

Conclusion

Reliable REST API pagination links make navigation state explicit and reduce assumptions in client code. Pick a model that fits the dataset, return links clients can follow directly, and document what happens when results change or a cursor is no longer valid.

Research note

Live documentation retrieval was not available while preparing this article, so formal standards and platform guidance could not be checked against their current text. The examples describe common API design patterns; verify the Web Linking syntax and any platform-specific pagination conventions against the authoritative documentation used by your implementation before standardizing them.