Skip to content
Back to skills

Api Pagination

ASecurity

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.

  • 9 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 6, 2026
ai-agentssqlnodeapi

Works with

  • cursor
  • cli
  • api

Security analysis

A100/100

Pro scans all 7 files and shows the line behind each finding

Scanned September 6, 2026

npx -y skills add vanara-agents/skills --skill api-pagination --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Api Pagination?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Api Pagination
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/vanara-agents-api-pagination/badge)](https://www.skillsdirectory.com/skills/vanara-agents-api-pagination)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
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:

```sql
-- 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

```json
{
  "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).

Files in this skill

  • SKILL.md3.9 KB
  • examples/keyset-queries.sql1.3 KB
  • examples/response-shapes.md1.5 KB
  • references/consuming-pages.md1.8 KB
  • references/counting-and-totals.md1.7 KB
  • references/cursor-encoding.md1.8 KB
  • scripts/check-cursor.mjs2.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.