python 84 lines · 3 tabs

Cursor-Based Pagination for a Flask JSON API Blueprint

Shared by codesnips Aug 2026
3 tabs
import base64
import json
from datetime import datetime


class InvalidCursor(Exception):
    pass


def encode_cursor(created_at, row_id):
    payload = {"t": created_at.isoformat(), "id": row_id}
    raw = json.dumps(payload, separators=(",", ":")).encode("utf-8")
    return base64.urlsafe_b64encode(raw).decode("ascii")


def decode_cursor(token):
    try:
        raw = base64.urlsafe_b64decode(token.encode("ascii"))
        payload = json.loads(raw)
        created_at = datetime.fromisoformat(payload["t"])
        row_id = int(payload["id"])
    except (ValueError, KeyError, TypeError):
        raise InvalidCursor("cursor is malformed")
    return created_at, row_id
3 files · python Explain with highlit

This snippet shows how to build stable, efficient pagination for a JSON API using keyset (cursor) pagination instead of LIMIT/OFFSET. Offset pagination degrades on large tables because the database still scans and discards every skipped row, and rows shifting between requests cause items to be duplicated or missed. Keyset pagination avoids both problems by remembering the last row seen and asking for rows strictly after it, using an indexed ordering column.

In cursor.py, the cursor is a small opaque token rather than a raw offset. encode_cursor serializes the ordering values — a timestamp and the row id as a tiebreaker — into URL-safe base64 JSON, and decode_cursor reverses it while treating any malformed input as a 400 via InvalidCursor. Encoding the values keeps the API contract loose: clients pass the token back verbatim without depending on its internal shape, which lets the server evolve the sort key later.

In queries.py, paginate_articles implements the actual keyset seek. Because ordering is on (created_at, id) descending, the WHERE clause uses a row-value comparison expressed with tuple_(...) < tuple_(...), which correctly handles rows sharing the same created_at. This compound key is essential — ordering by a non-unique column alone would let the cursor land ambiguously between equal rows. It fetches limit + 1 rows so it can detect whether a further page exists without a separate COUNT, then trims the extra row and reports has_more.

In articles_bp.py, the Flask blueprint wires this into an endpoint. list_articles clamps the client-supplied limit to a sane maximum to prevent unbounded queries, decodes an optional cursor, and calls the query helper. The response embeds a next_cursor built from the last returned row, so clients follow the chain by echoing that value. The InvalidCursor handler returns a clean JSON 400 rather than leaking a stack trace.

The trade-off is that keyset pagination only supports forward/backward stepping, not random jumps to page N, and requires an index on the sort tuple. For feeds, timelines, and infinite scroll — where users move sequentially — it is the more scalable and correct choice.


Related snips

Share this code

Here's the card — post it anywhere.

Cursor-Based Pagination for a Flask JSON API Blueprint — share card
Link copied