Pagination Project
DodaTech
2 min read
title: "Pagination Project — Build a Production Pagination System" description: "Build a complete production pagination system with offset and cursor modes, link headers, sorting, filtering, and encoding in this hands-on pagination project." date: 2026-06-28 lastmod: 2026-06-28 weight: 25 tags: [apis, pagination] }
Build a production-ready pagination system implementing both offset and cursor modes with consistent metadata, error handling, and performance monitoring.
What You'll Learn
- Building a dual-mode pagination system
- Implementing link headers and metadata
- Performance testing both approaches
Why It Matters
This project combines all pagination concepts into a single, deployable pagination module you can use in any API.
Project Setup
from flask import Flask, request, jsonify
import base64, json, hmac, hashlib, math, time
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
# In-memory database
users = [
{"id": i, "name": f"User {i}", "email": f"user{i}@example.com",
"created_at": f"2026-06-{min(28, i):02d}"}
for i in range(1, 1001)
]
Implementation
def encode_cursor(value):
payload = json.dumps({"v": value, "t": int(time.time())})
sig = hmac.new(app.config['SECRET_KEY'].encode(),
payload.encode(), hashlib.sha256).hexdigest()[:16]
encoded = base64.urlsafe_b64encode(payload.encode()).decode()
return f"{encoded}.{sig}"
def decode_cursor(cursor):
try:
encoded, sig = cursor.split(".")
expected = hmac.new(app.config['SECRET_KEY'].encode(),
encoded.encode(), hashlib.sha256).hexdigest()[:16]
if not hmac.compare_digest(sig, expected):
return None
payload = base64.urlsafe_b64decode(encoded.encode())
data = json.loads(payload)
if time.time() - data["t"] > 3600:
return None
return data["v"]
except Exception:
return None
@app.route('/users')
def list_users():
mode = request.args.get('mode', 'offset')
limit = min(request.args.get('limit', 20, type=int), 100)
sort_by = request.args.get('sort_by', 'id')
sort_order = request.args.get('sort_order', 'asc')
if mode == 'cursor':
return cursor_pagination(limit, sort_by, sort_order)
return offset_pagination(limit, sort_by, sort_order)
def offset_pagination(limit, sort_by, sort_order):
page = request.args.get('page', 1, type=int)
total = len(users)
total_pages = math.ceil(total / limit)
start = (page - 1) * limit
data = sorted(users, key=lambda u: u[sort_by],
reverse=(sort_order == 'desc'))[start:start+limit]
return jsonify({
"data": data,
"pagination": {
"page": page, "per_page": limit, "total": total,
"total_pages": total_pages,
"has_next": page < total_pages,
"has_prev": page > 1
}
})
def cursor_pagination(limit, sort_by, sort_order):
cursor = decode_cursor(request.args.get('cursor'))
all_users = sorted(users, key=lambda u: u[sort_by],
reverse=(sort_order == 'desc'))
start = 0
if cursor:
for i, u in enumerate(all_users):
if u[sort_by] == cursor or u['id'] == cursor:
start = i + 1
break
data = all_users[start:start+limit]
next_cursor = encode_cursor(data[-1][sort_by]) if len(data) == limit else None
return jsonify({
"data": data,
"pagination": {
"has_more": len(data) == limit,
"next_cursor": next_cursor
}
})
Testing
# Test cases
def test_pagination():
# Test offset mode
res = app.test_client().get('/users?mode=offset&page=1&limit=5')
assert res.json['pagination']['page'] == 1
assert len(res.json['data']) == 5
# Test cursor mode
res = app.test_client().get('/users?mode=cursor&limit=5')
cursor = res.json['pagination']['next_cursor']
res2 = app.test_client().get(f'/users?mode=cursor&limit=5&cursor={cursor}')
assert res2.json['data'][0]['id'] > res.json['data'][-1]['id']
# Test invalid cursor
res = app.test_client().get('/users?mode=cursor&cursor=invalid')
assert res.status_code == 200
assert res.json['data'] is not None
Challenge
Extend this project with:
- Sorting with tiebreaker column
- Filtering (status, role, date range)
- Link headers in offset mode
- Performance monitoring (track query time per page)
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro