Skip to content
Development
Skill

/api-pagination

Implement correct, fast API pagination — cursor vs offset trade-offs, opaque cursor encoding, stable sort keys, page-size limits, total-count costs, and consumer-side iteration that survives inserts, deletes, and retries. With runnable cursor checks.

From plugin
vanara-agents-skills
917 skills54 agents
Install
$ npx -y skills add vanara-agents/skills --skill api-pagination --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/api-pagination

Context preview

The summary Claude sees to decide when to auto-load this skill.

Implement correct, fast API pagination — cursor vs offset trade-offs, opaque cursor encoding, stable sort keys, page-size limits, total-count costs, and consumer-side iteration that survives inserts, deletes, and retries. With runnable cursor checks.

SKILL.md

api-pagination.SKILL.md
name: api-pagination
description: Implement correct, fast API pagination — cursor vs offset trade-offs, opaque cursor encoding, stable sort keys, page-size limits, total-count costs, and consumer-side iteration that survives inserts, deletes, and retries. With runnable cursor checks.
type: skill
version: 1.0.0
updated: 2026-07-10

API Pagination

Pagination looks trivial until data changes underneath the reader. Offset pagination silently **skips or duplicates rows** when inserts land mid-iteration; cursor pagination survives churn but demands a stable sort key and an opaque token contract. This skill covers both sides: designing the API and consuming one correctly. Deep detail lives in `references/`; copy-paste material in `examples/`; a runnable cursor round-trip check in `scripts/`.

Decision: offset vs cursor

| Situation | Use | |---|---| | Admin tables, small datasets, "jump to page 7" required | Offset (`LIMIT/OFFSET`) | | Feeds, sync endpoints, anything users scroll | Cursor (keyset) | | Data changes while clients iterate | Cursor — offset will skip/duplicate | | Deep pages (offset > ~10k rows) | Cursor — `OFFSET n` scans and discards n rows |

Offset's failure mode is correctness, not just speed: a row inserted before your current position shifts everything, so page 3 re-shows an item from page 2 (duplicate) or swallows one (skip).

Keyset mechanics (the part people get wrong)

The sort key must be **unique and immutable**. `created_at` alone is not unique — two rows with the same timestamp make the cursor ambiguous and rows vanish. Always add a tiebreaker:

-- WHERE clause for "next page after (2026-07-01T12:00:00Z, id 4711)" descending
SELECT * FROM items
WHERE (created_at, id) < ('2026-07-01T12:00:00Z', 4711)
ORDER BY created_at DESC, id DESC
LIMIT 51;  -- page_size + 1 to detect has_more without COUNT(*)

Fetch `page_size + 1` rows: if you get 51, there is a next page and the 51st row's key becomes the next cursor. This avoids `COUNT(*)`, which on large tables costs more than the page itself (see `references/counting-and-totals.md`).

Cursor token contract

Cursors are **opaque to clients** — never let them parse or build one. Encode the keyset values plus the sort direction, base64url it, and (for public APIs) HMAC it so tampering is detectable. Include a version byte so you can change the encoding later. Full recipe: `references/cursor-encoding.md`; runnable round-trip: `scripts/check-cursor.mjs`.

Response envelope

{
  "data": [ ... ],
  "page_info": { "has_more": true, "next_cursor": "djEuMj..." }
}

Rules: `next_cursor` is absent (not empty-string) on the last page; page size is capped server-side (a `limit=10000` request gets the cap, documented, not an error); the default page size appears in the docs and never changes silently. Consumer-side iteration rules — retries, cursor expiry, resumption — live in `references/consuming-pages.md`.

Pitfalls

  • **Mutable sort keys** (`updated_at`) — rows teleport across pages as they update. Sort on immutable

columns or accept re-delivery and dedupe client-side.

  • **Cursor built from row position** instead of row values — it is offset with extra steps and

inherits every offset bug.

  • **`COUNT(*)` on every page** — the count is stale by the time the client renders it; provide totals

on a separate, cacheable endpoint if the product truly needs them.

  • **Breaking cursor encoding on deploy** — clients hold cursors across releases; version the token.
  • **Missing composite index** — keyset needs an index on exactly `(created_at, id)` in sort order, or

every page is a scan.

Verification

Run `node scripts/check-cursor.mjs` — encodes, decodes, tampers, and versions a cursor and asserts every property above. For the SQL side, `examples/keyset-queries.sql` includes an `EXPLAIN` you can run to confirm the index is used. Pairs with `rest-api-design` (envelopes, limits) and `sql-index-tuning` (composite indexes).

Read more
Ships withvanara-agents-skills

🐒 Free agents, skills & packs for Claude Code One subscription. An army of Claude Code agents. 30 production-grade agents, skills, and packs for Claude Code — free, Apache-2.0, install with one command.

Get the whole plugin

Other skills on vanara-agents-skills.