Pagination in REST APIs — Complete Guide
In this tutorial, you will learn about Pagination in REST APIs. We cover key concepts, practical examples, and best practices to help you master this topic.
Pagination in REST APIs splits large result sets into manageable pages, using offset-based pagination with page numbers or cursor-based pagination with opaque tokens for consistent iteration.
flowchart TD A[Pagination Types] --> B[Offset-Based] A --> C[Cursor-Based] A --> D[Keyset Pagination] B --> B1[?page=2&per_page=20] B --> B2[Simple but inconsistent] C --> C1[?cursor=abc123] C --> C2[Consistent across changes] style B fill:#fff9c4 style C fill:#c8e6c9
Offset-based pagination uses ?page=2&per_page=20. The server returns items 20-39. It is simple but has a problem: if items are inserted or deleted between requests, the same item may appear on two pages or be skipped. Cursor-based pagination uses an opaque token pointing to a specific item: ?cursor=abc123&limit=20. It returns items after the cursor, which is stable even when data changes.
Think of offset pagination like reading a book where page numbers can shift if pages are added or removed. Cursor pagination is like using a bookmark. You always continue from where you left off, regardless of what changes around you.
Example: Offset-Based Pagination
import requests
def get_users_page(page, per_page=10):
response = requests.get(
"https://api.example.com/users",
params={"page": page, "per_page": per_page}
)
data = response.json()
total = response.headers.get("X-Total-Count")
print(f"Page {page}: {len(data)} users (total: {total})")
return data
# Fetch first 3 pages
for p in range(1, 4):
get_users_page(p)
Expected output:
Page 1: 10 users (total: 150)
Page 2: 10 users (total: 150)
Page 3: 10 users (total: 150)
Example: Cursor-Based Pagination
import requests
def get_users_cursor(cursor=None, limit=10):
params = {"limit": limit}
if cursor:
params["cursor"] = cursor
response = requests.get(
"https://api.example.com/users",
params=params
)
data = response.json()
next_cursor = data.get("next_cursor")
print(f"Got {len(data['items'])} users, next: {next_cursor}")
return next_cursor
# Fetch all pages using cursor
cursor = None
page_num = 0
while cursor is not None or page_num == 0:
cursor = get_users_cursor(cursor)
page_num += 1
if page_num >= 5 or not cursor:
break
Expected output:
Got 10 users, next: abc123
Got 10 users, next: def456
Got 10 users, next: ghi789
Got 10 users, next: None
Example: Pagination with Link Header
import requests
response = requests.get(
"https://api.example.com/users",
params={"page": 2, "per_page": 10}
)
link_header = response.headers.get("Link")
print(f"Link header: {link_header}")
# Parse Link header
links = {}
for part in link_header.split(", "):
section = part.split("; ")
url = section[0].strip("<>")
rel = section[1].split("=")[1].strip("\"")
links[rel] = url
print(f"First: {links.get('first')}")
print(f"Prev: {links.get('prev')}")
print(f"Next: {links.get('next')}")
print(f"Last: {links.get('last')}")
Expected output:
Link header: <https://api.example.com/users?page=1&per_page=10>; rel="first", <https://api.example.com/users?page=1&per_page=10>; rel="prev", <https://api.example.com/users?page=3&per_page=10>; rel="next", <https://api.example.com/users?page=15&per_page=10>; rel="last"
First: https://api.example.com/users?page=1&per_page=10
Prev: https://api.example.com/users?page=1&per_page=10
Next: https://api.example.com/users?page=3&per_page=10
Last: https://api.example.com/users?page=15&per_page=10
Common Mistakes
- Not providing pagination at all — Returning all results without pagination causes memory issues on both server and client for large datasets.
- Using offset pagination for real-time data — Offset pagination skips or duplicates items when data changes between requests. Use cursor-based pagination for dynamic datasets.
- Not setting a maximum per_page — Letting clients request 10000 items per page defeats the purpose of pagination and can crash the server.
- Forgetting pagination metadata — Include total count, page number, and next/prev links so clients can navigate without guessing.
- Inconsistent pagination parameter names — Using page/per_page in some endpoints and offset/limit in others forces clients to handle both patterns.
Practice Questions
- What is the main advantage of cursor pagination over offset pagination?
- What headers are used for pagination links in REST APIs?
- Why should you limit the maximum items per page?
- When would you choose offset pagination over cursor pagination?
- Challenge: Implement a cursor-based pagination system in Python that generates opaque cursors encoding the last seen ID and a timestamp. Decode cursors to resume pagination.
FAQ
Mini Project
Build a Python pagination utility class that supports both offset-based and cursor-based pagination. It should generate pagination URLs, calculate offsets, encode/decode cursors, and include metadata in the response envelope.
What's Next
Now learn about HATEOAS in REST API Design.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro