HATEOAS Pagination — Hypermedia-Driven Pagination with Link Relations
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
Example 1: Link-Based Pagination Response
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
Example 3: Pagination Client Following Links
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.
2. Missing first and last Links
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
- What rel values are standard for pagination?
- How is cursor-based pagination different from page-based?
- Why should clients follow
nextlinks instead of incrementing page numbers? - What happens when a page has no
nextlink? - How do you handle concurrent writes during cursor pagination?
Answers:
first,last,next,prev, andself.- Cursor uses an opaque token (e.g., last item ID); page uses numeric page numbers.
- The server controls navigation — it can skip empty pages, adjust ordering, or inject advertisements.
- The client has consumed all available items and should stop.
- 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
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