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 Parse API Pagination URLs Without Losing Parameters

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

October 28, 20267 min read5 topics
How to Parse API Pagination URLs Without Losing Parameters
Explore the details
#API pagination#REST APIs#JavaScript#URL parsing#HTTP
October 28, 20267 min read

When an API response includes a link to the next page, it may contain more than a page number: it can carry a cursor, a page-size limit, repeated filters, or other state. Rebuilding that URL by hand can silently drop parameters and produce incomplete results. This tutorial shows how to inspect a pagination URL with JavaScript and how to follow the server-provided link intact.

The examples use a fictional JSON response shape. Pagination fields and the rules for resolving relative links vary by API, so check your API's documentation for the exact field name and link behavior. I could not retrieve vendor documentation while preparing this guide; the examples do not claim to describe any particular vendor's schema.

Key Takeaways

  • Prefer the server-provided next-page link over constructing a URL from guessed parameters.
  • Use JavaScript's URL and URLSearchParams to inspect URL components and query values.
  • Use getAll() when a parameter may appear more than once; get() returns only one value.
  • Resolve relative links against the documented base URL, commonly the URL of the response that supplied the link.
  • Follow the API's pagination documentation: the next link may be in the JSON body, an HTTP Link header, or another documented field.

What a pagination URL can contain

An API may paginate with page numbers, offsets, cursors, or a combination of these. A next link can also preserve filters, sorting, field selections, and page-size settings. For example:

https://api.example.test/v1/orders?limit=50&cursor=eyJvZmZzZXQiOjUwfQ%3D%3D&include=items&include=customer

Here, limit sets a page size, cursor carries an opaque position token, and include appears twice. Treat cursor values as opaque: inspect them if useful for debugging, but do not assume you can decode or modify them safely. The API defines their meaning.

Inspect a next-page URL with JavaScript

The URL class separates the URL into components, while its searchParams property provides query-parameter access:

const nextLink =
  "https://api.example.test/v1/orders?limit=50&cursor=eyJvZmZzZXQiOjUwfQ%3D%3D&include=items&include=customer";

const url = new URL(nextLink);

console.log(url.origin);                 // https://api.example.test
console.log(url.pathname);               // /v1/orders
console.log(url.searchParams.get("limit"));
// 50
console.log(url.searchParams.get("cursor"));
// eyJvZmZzZXQiOjUwfQ==
console.log(url.searchParams.getAll("include"));
// ["items", "customer"]

URLSearchParams returns percent-decoded values. For example, %3D in the cursor becomes = when read. Do not decode a value again without a specific reason: double-decoding can change its meaning. Query strings can also use + to represent a space under form-style query parsing, so inspect the parsed value rather than assuming the raw text is the decoded value.

If you need to inspect a raw query string from a relative link, first resolve it to a URL with an appropriate base:

const currentResponseUrl = "https://api.example.test/v1/orders?limit=50";
const relativeNextLink = "?limit=50&cursor=next-token";

const nextUrl = new URL(relativeNextLink, currentResponseUrl);
console.log(nextUrl.href);
// https://api.example.test/v1/orders?limit=50&cursor=next-token

For an API that documents a different base for relative links, use that documented base instead. When parsing a Link header or a JSON next field, follow the API's documented syntax; do not assume every response uses the same format.

Follow the link instead of rebuilding it

A common mistake is to read a cursor and then construct the next request from only the parameters you recognize. That can omit filters or other server-managed state. If the API supplies a next link, use that link as the next request URL.

This example assumes each response has a JSON shape like { "data": [...], "links": { "next": "..." } }. Adapt the field names to your API's documented response format. It supports absolute and relative next links, detects a repeated URL to avoid an accidental loop, and checks that pagination stays on the starting origin before making another request.

async function fetchAllOrders(startUrl) {
  const start = new URL(startUrl);
  const allowedOrigin = start.origin;
  let currentUrl = start;
  const visited = new Set();
  const orders = [];

  while (currentUrl) {
    if (currentUrl.origin !== allowedOrigin) {
      throw new Error("Pagination link changed origin");
    }

    if (visited.has(currentUrl.href)) {
      throw new Error("Pagination link repeated; stopping to avoid a loop");
    }
    visited.add(currentUrl.href);

    const response = await fetch(currentUrl);
    if (!response.ok) {
      throw new Error(`Request failed: ${response.status} ${response.statusText}`);
    }

    const page = await response.json();
    if (!Array.isArray(page.data)) {
      throw new Error("Expected the response to contain a data array");
    }
    orders.push(...page.data);

    const next = page.links?.next;
    if (next == null || next === "") {
      break;
    }
    if (typeof next !== "string") {
      throw new Error("Expected the next link to be a string");
    }

    // Relative links are resolved against the URL of the response that supplied them.
    const baseUrl = response.url || currentUrl.href;
    currentUrl = new URL(next, baseUrl);
  }

  return orders;
}

const orders = await fetchAllOrders(
  "https://api.example.test/v1/orders?limit=50&status=open"
);
console.log(`Fetched ${orders.length} orders`);

This example collects every record in memory, which may be unsuitable for a very large result set. For large collections, process each page as it arrives or use an async iterator rather than accumulating all records.

The origin check is a useful safeguard for a client that expects pagination to stay on one API origin; it is not a substitute for the API's authentication, authorization, or other security controls. If an API documents cross-origin pagination, use an explicit allowlist and understand how your HTTP client handles credentials and redirects before following those links.

Edge cases to check

Repeated query parameters

A query can contain the same key more than once, as with include=items&include=customer. searchParams.get("include") returns only one value; use getAll("include") to inspect them all. Avoid converting parameters into a plain object if duplicates matter. Also note that URLSearchParams.set() replaces existing values for a key, while append() adds another value.

Encoded values

Percent-encoded characters are normal in query values, especially in opaque cursors. Parsing gives you decoded values; constructing or serializing a URL may produce a different but equivalent encoding. For that reason, do not parse a server-provided next link and serialize a hand-built replacement unless you need to change it and understand the API's rules.

Absolute and relative links

An absolute link includes a scheme and host. A relative link needs a base URL to become usable. Resolve it with new URL(link, baseUrl) rather than concatenating strings. Confirm the base-resolution rule in the API documentation, particularly when the response was redirected or when a pagination link comes from an HTTP header.

Missing or repeated next links

A missing or null next link commonly indicates there is no further page, but use the API's documented end-of-pagination signal. A repeated link can cause an endless loop if a server or client is misbehaving; tracking visited URLs helps detect that failure. Consider adding a maximum-page limit or cancellation mechanism for jobs that must have a hard resource bound.

Common Mistakes

  • Appending a guessed cursor to the first URL: This can discard filters, sorting, or other parameters. Follow the supplied link when the API provides one.
  • Using get() for a repeated parameter: It does not return every value; use getAll() when duplicates are possible.
  • Treating a cursor as an ordinary page number: Cursors may be opaque and meaningful only to the API that issued them.
  • Concatenating relative links as strings: Resolve them against the documented base with new URL().
  • Assuming every API uses the same response shape: Check whether pagination is represented in a JSON field, a Link header, or another documented mechanism.
  • Trusting a link without considering its destination: If the client expects one API origin, validate the destination before sending the request, and account for redirects and credential handling.

Best Practices

  1. Read the API's pagination documentation before writing the page loop. Identify the next-link location, termination signal, and relative-link base.
  2. Preserve the server-provided URL as-is whenever possible; parse it for diagnostics, not to reconstruct it unnecessarily.
  3. Use URL and URLSearchParams rather than manual splitting or string concatenation.
  4. Test with encoded cursors, repeated parameters, relative links, and a final page with no next link.
  5. Add appropriate failure handling, loop detection, and resource limits for the integration's workload.

Conclusion

To parse an API pagination URL safely, inspect it with the platform URL APIs, account for repeated and encoded query values, and follow the API's documented next link rather than rebuilding it from assumptions. That keeps the parameters the server supplied and makes pagination behavior easier to debug.