Designing API Link Headers: Pagination Discovery That Survives URL Schema Changes
Most pagination tutorials show you how to build cursor URLs from response metadata. RFC 8288 Link headers turn that into the API's responsibility, so clients follow URLs without constructing them. The result is fewer client-side bugs and more freedom to evolve the URL scheme.
The textbook pagination response includes a body with a cursor field, and the client constructs the next-page URL by appending the cursor as a query parameter. The pattern works, but it couples the client to the URL schema in a way that makes future changes painful. Adding query parameters, renaming the cursor field, switching from query string to path segment, or changing the pagination algorithm all require coordinated client updates. RFC 8288 Link headers move the URL construction responsibility from the client to the server, with the trade-off that clients have to read headers rather than just parse the response body.
What Link headers actually do
The Link header is a comma-separated list of URI references, each with a rel attribute specifying the relationship to the current resource. For pagination, the relevant rel values are next, prev, first, and last. A canonical example from the GitHub API:
Link: <https://api.example.com/v1/issues?cursor=abc123>; rel="next",
<https://api.example.com/v1/issues?cursor=def456>; rel="last"The client requesting the next page does not parse cursor=abc123 or construct anything: it follows the URL the server provided in the next rel. The server can change the URL format, the cursor encoding, or the entire pagination algorithm without breaking clients, because clients are following declared URLs rather than constructing them from documented rules.
The header can include any combination of relations. Forward-only pagination provides just next; bounded result sets provide next plus last. Bidirectional iteration adds prev. The first relation is useful for cursor-based pagination because clients with stale cursors can recover by following first to restart.
Why most APIs do not use Link headers
The dominant pattern in modern API design is to put pagination metadata in the response body, in a structure like {"data": [...], "next_cursor": "abc123"}. This pattern is simpler for client developers because they parse one thing (the JSON body) rather than two things (the body plus the headers). Most API tutorials, SDK generators, and client libraries assume body-based pagination, which creates a substantial inertia toward that pattern.
The arguments for body-based pagination are real. JavaScript developers in particular tend to find header parsing awkward because the Fetch API does not expose headers as conveniently as JSON. Documentation tools render JSON bodies more naturally than HTTP headers. Test fixtures and recorded API traffic are easier to inspect for body-based pagination because the cursor is visible in the captured payload.
The arguments against body-based pagination are about coupling. Every client that uses your API for pagination encodes assumptions about the body shape: the cursor field name, the URL construction rules, the parameter order, the encoding of compound cursors. Changes to any of these break clients. Link headers eliminate the assumptions by making the client follow declared URLs, which puts the URL schema entirely under server control.
The hybrid pattern
Most APIs that take pagination headers seriously also keep body-based metadata, because the cost of duplication is small and the audience for each is different. The body cursor is for developers who prefer the convention or are using SDKs that expect it. The Link header is for developers who want decoupling, for HTTP clients that follow Link headers automatically (some web crawlers, some API browsers), and for cases where the URL schema needs to change.
The hybrid pattern looks like:
HTTP/1.1 200 OK
Content-Type: application/json
Link: <https://api.example.com/v1/issues?cursor=abc123>; rel="next"
{
"data": [...],
"next_cursor": "abc123",
"next_url": "https://api.example.com/v1/issues?cursor=abc123"
}The body and header carry the same information in different forms, and clients pick whichever is more convenient. The cost is small (a few hundred bytes per response) and the flexibility is substantial.
The discoverability benefit
Link headers compose with the broader REST convention of HATEOAS (hypermedia as the engine of application state). The idea is that responses include navigation links to related resources, which lets clients discover capabilities rather than encoding them. Pagination is the most common application of the principle, but the same mechanism extends to relationships (a user response includes Link: rel="orders" pointing at the user's orders endpoint), actions (a draft response includes Link: rel="publish" pointing at the publish action), and metadata (a collection response includes Link: rel="schema" pointing at the JSON schema for the resource type).
Most APIs do not go this far because the complexity does not match the benefit at small scale. But pagination is the one place where the cost is minimal and the benefit is real: clients that follow Link headers for pagination are protected against URL schema changes that would otherwise require coordinated updates.
The implementation
The server-side implementation is straightforward. The pagination handler computes the next cursor as usual, constructs the full URL with the cursor appended, and adds the Link header before sending the response. The framework convention is usually to set the header via a middleware that runs after the handler determines the next cursor:
response.headers.add('Link', f'<{next_url}>; rel="next"')The trick is that the URL must be a full URL including the scheme and host, not just a path. Clients following Link headers do not assume any base URL; they expect the URL to be complete. The convention is to use the same scheme and host that the request came in on, which means the server must consult the request URL rather than hard-coding production URLs.
For APIs behind a reverse proxy or CDN, the host needs to come from the X-Forwarded-Host or Forwarded header rather than the Host header the application sees directly. Getting this right requires some care; the failure mode is Link headers that point at internal hostnames or wrong ports, which breaks clients silently.
Three patterns that fail
First, Link headers with relative URLs. Some APIs return Link: /v1/issues?cursor=abc123; rel="next". The HTTP spec permits this (the URL is resolved relative to the request URL), but many client libraries do not handle relative URLs in Link headers correctly. The safe pattern is absolute URLs always.
Second, Link headers without the cursor URL in the body. If your API uses Link headers only, clients that prefer body-based pagination will be unable to use your API conveniently. The hybrid pattern (Link plus body cursor plus body next_url) costs almost nothing and serves both audiences.
Third, Link headers that change semantics between versions. If your v1 API returns Link with absolute URLs and v2 returns Link with relative URLs, you have effectively broken backwards compatibility. The Link header behavior should be stable across API versions.
Our use across the four products
DocuMint, CronPing, FlagBit, and WebhookVault all use the hybrid pattern. The Link header is set on every paginated endpoint via shared FastAPI middleware that reads the response body's next_cursor field and constructs the full URL. The body retains the cursor and full URL for clients that want them. The Link header is documented in our API reference but not emphasized; most customers use the body-based pattern from the SDK, and a small minority use the Link header directly.
The deeper observation is that adding Link headers is one of the small architectural investments that pays back in proportion to how long the API is in production. The cost is one middleware function, one documentation paragraph, and a small bandwidth increase per response. The benefit is the ability to evolve URL schemas without coordinating client updates, which is the kind of flexibility that compounds over multiple years.
Our products: DocuMint (PDF invoice generation API), CronPing (cron job monitoring with status pages), FlagBit (feature flags API for modern teams), and WebhookVault (webhook capture and replay) put these patterns into production.