Skip to content

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:

  1. Sorting with tiebreaker column
  2. Filtering (status, role, date range)
  3. Link headers in offset mode
  4. Performance monitoring (track query time per page)

FAQ

How do I deploy this pagination system?

: Package as a Flask blueprint or FastAPI dependency for reuse.

Should I support both modes?

: Yes. Let clients choose offset or cursor mode per request.

How do I paginate in other databases?

: Use the same concept with database-specific syntax (e.g., DynamoDB ExclusiveStartKey).

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro