pagination-patterns — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited pagination-patterns (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Any task involving list endpoint pagination, cursor vs offset strategies, infinite scroll implementation, total count optimization, or pagination API design.
Implementation:
SELECT * FROM orders
WHERE tenant_id = 'abc'
ORDER BY created_at DESC
LIMIT 20 OFFSET 40; -- page 3API design:
GET /orders?page=3&per_page=20
{
"data": [...],
"meta": {
"page": 3,
"per_page": 20,
"total": 1542,
"total_pages": 78
}
}Pros:
Cons:
Best for: admin panels, back-office UIs, small datasets (< 100K rows).
Implementation:
-- First page
SELECT * FROM orders
WHERE tenant_id = 'abc'
ORDER BY created_at DESC, id DESC
LIMIT 20;
-- Next page (cursor = last seen created_at + id)
SELECT * FROM orders
WHERE tenant_id = 'abc'
AND (created_at, id) < ('2025-01-15T10:00:00Z', 'order-xyz')
ORDER BY created_at DESC, id DESC
LIMIT 20;API design:
GET /orders?limit=20&after=eyJjcmVhdGVkX2F0IjoiMjAyNS0wMS0xNVQxMDowMDowMFoiLCJpZCI6Im9yZGVyLXh5eiJ9
{
"data": [...],
"meta": {
"has_next": true,
"next_cursor": "eyJjcmVhdGVkX...",
"has_previous": true,
"previous_cursor": "eyJjcmVhdGVk..."
}
}Pros:
Cons:
Best for: feeds, timelines, infinite scroll, large datasets, real-time data.
Rules:
Example encoding:
// Encode
const cursor = Buffer.from(JSON.stringify({
created_at: lastItem.createdAt,
id: lastItem.id
})).toString('base64url');
// Decode
const { created_at, id } = JSON.parse(Buffer.from(cursor, 'base64url').toString());type OrderConnection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
totalCount: Int # nullable — expensive to compute
}
type OrderEdge {
node: Order!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}Usage:
first: 20, after: "cursor" — forward pagination.last: 20, before: "cursor" — backward pagination.Problem: SELECT COUNT(*) FROM large_table is slow (full table scan in PostgreSQL).
Solutions:
| Strategy | Accuracy | Performance | Use case |
|---|---|---|---|
| Exact count (separate query) | 100% | Slow (large tables) | Admin UIs, small datasets |
Estimated count (pg_stat_user_tables.reltuples) | ~95% | Instant | "About X results" display |
Count with cap (COUNT(*) ... LIMIT 1001) | Exact up to cap | Fast | "1000+ results" |
| Cached count (materialized/Redis) | Stale by TTL | Instant | Dashboards, frequently queried |
| No count (cursor only) | N/A | N/A | Infinite scroll, feeds |
Recommendation: Make totalCount optional in the API. Let clients request it only when needed.
Frontend pattern:
function useInfiniteList(fetchPage) {
const [pages, setPages] = useState([]);
const [cursor, setCursor] = useState(null);
const [hasMore, setHasMore] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const loadMore = async () => {
if (isLoading || !hasMore) return;
setIsLoading(true);
const result = await fetchPage(cursor);
setPages(prev => [...prev, result.data]);
setCursor(result.nextCursor);
setHasMore(result.hasNext);
setIsLoading(false);
};
// Trigger loadMore on scroll near bottom
// Virtualize rendered list for performance
}Rules:
REST:
per_page=999999).GraphQL:
General:
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.