Skip to content

HATEOAS Pagination — Hypermedia-Driven Pagination with Link Relations

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about HATEOAS Pagination. We cover key concepts, practical examples, and best practices to help you master this topic.

HATEOAS pagination uses link relations like first, last, next, and prev to navigate paginated collections through discoverable links, making pagination self-documenting and client-agnostic.

What You'll Learn

  • Link-based pagination with standard rel values
  • Cursor-based pagination via hypermedia links
  • Page size negotiation
  • Combining pagination with ETags for efficient re-fetching
  • Testing hypermedia pagination flows

Why It Matters

Traditional pagination requires clients to construct URLs by incrementing page numbers. Hypermedia pagination removes this dependency — clients simply follow next links. DodaTech's IOC database serves millions of indicators with hypermedia pagination, letting clients consume pages without knowing page structure.

Real-World Use

A threat intelligence API returns paginated collections of IoCs. Clients follow next links to consume all indicators. When new data arrives, the next link automatically adjusts. Clients never construct URLs — they just follow links.

flowchart LR
    A["Entry: /iocs"] --> B["Page 1
links: first, next, last"] B --> C["Page 2
links: prev, self, next, last"] C --> D["Page N
links: prev, self, first"] B -.->|"follow next"| C C -.->|"follow next"| D

Code Examples

def paginate(query, page, per_page):
    total = query.count()
    total_pages = (total + per_page - 1) // per_page
    items = query.offset((page - 1) * per_page).limit(per_page).all()

    base_url = '/iocs'
    links = {
        'self': {'href': f'{base_url}?page={page}&per_page={per_page}'},
        'first': {'href': f'{base_url}?page=1&per_page={per_page}'},
        'last': {'href': f'{base_url}?page={total_pages}&per_page={per_page}'}
    }
    if page > 1:
        links['prev'] = {'href': f'{base_url}?page={page-1}&per_page={per_page}'}
    if page < total_pages:
        links['next'] = {'href': f'{base_url}?page={page+1}&per_page={per_page}'}

    return {
        'data': [item.to_dict() for item in items],
        'page': page,
        'per_page': per_page,
        'total': total,
        '_links': links
    }

# Usage
result = paginate(threat_query, page=2, per_page=10)
print(result['_links']['next']['href'])
# Output: /iocs?page=3&per_page=10

Example 2: Cursor-Based Pagination with Hypermedia

def cursor_paginate(query, cursor=None, limit=20):
    if cursor:
        items = query.filter(Query.id > cursor).limit(limit + 1).all()
    else:
        items = query.limit(limit + 1).all()

    has_more = len(items) > limit
    items = items[:limit]
    next_cursor = items[-1].id if items and has_more else None

    links = {'self': {'href': f'/iocs?limit={limit}'}}
    if cursor:
        links['prev'] = {'href': f'/iocs?before={cursor}&limit={limit}'}
    if next_cursor:
        links['next'] = {'href': f'/iocs?after={next_cursor}&limit={limit}'}

    return {
        'data': [item.to_dict() for item in items],
        '_links': links,
        'count': len(items)
    }

result = cursor_paginate(ioc_query, cursor='abc123')
print(result['_links']['next']['href'])
# Output: /iocs?after=def456&limit=20
import requests

def consume_all_pages(entry_url):
    """Follow pagination links until no next page."""
    url = entry_url
    page_num = 1
    all_items = []

    while url:
        print(f"Fetching page {page_num}: {url}")
        response = requests.get(url, headers={'Accept': 'application/hal+json'})
        data = response.json()
        all_items.extend(data['data'])

        links = data.get('_links', {})
        url = links.get('next', {}).get('href')
        page_num += 1

    return all_items

iocs = consume_all_pages('https://api.dodatech.com/iocs')
print(f"Consumed {len(iocs)} indicators across multiple pages")
# Output: Consumed 5432 indicators across multiple pages

Common Mistakes

1. Hardcoding Page Numbers in Client

Clients should follow next links, not increment page counters manually.

Always provide first and last links so clients can reset navigation or jump to the end.

3. Changing Page Size Mid-Navigation

Once a client starts navigation, keep the page size consistent within that navigation session.

4. Not Including Total Count

Include total count or at least hasNextPage so clients know when to stop.

5. Cursor-Based Without Stability

Ensure cursors are stable — the same cursor should always return the same position, even if new items are added.

Practice Questions

  1. What rel values are standard for pagination?
  2. How is cursor-based pagination different from page-based?
  3. Why should clients follow next links instead of incrementing page numbers?
  4. What happens when a page has no next link?
  5. How do you handle concurrent writes during cursor pagination?

Answers:

  1. first, last, next, prev, and self.
  2. Cursor uses an opaque token (e.g., last item ID); page uses numeric page numbers.
  3. The server controls navigation — it can skip empty pages, adjust ordering, or inject advertisements.
  4. The client has consumed all available items and should stop.
  5. Use stable cursors (e.g., creation timestamps or IDs) that don't shift with new insertions.

Challenge: Build a paginated HATEOAS API for threat reports. Support both page-based and cursor-based pagination, and write a client that consumes all pages by following links.

FAQ

Should I use page-based or cursor-based pagination?

: Cursor-based is better for real-time data where new items arrive frequently. Page-based works for stable datasets.

What rel value should I use for the last page?

: Use last as the standard rel value for the final page link.

Can I combine pagination with search?

: Yes. Include query parameters in all pagination links: next includes the same search criteria.

How do I handle empty pages?

: Return an empty data array with the same link structure, but the next link may be absent.

Is pagination metadata included in the response?

: Yes. Include page, per_page, and total in the response body alongside links.

What's Next

After pagination, explore HATEOAS Performance to optimize large collections, and see how HATEOAS Conditional Requests help with cache efficiency.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro