As asked
Implement cursor-based pagination for a feed API in TypeScript. Explain why cursor beats offset for this case.
Sample answer outline
Offset pagination is O(offset) per page on the database (the query still scans the skipped rows) and breaks when items are inserted or deleted mid-paging. Cursor pagination uses an opaque token encoding the last-seen sort key (e.g. created_at + id for uniqueness). The next query is WHERE (created_at, id) < (last_created_at, last_id) ORDER BY created_at DESC, id DESC LIMIT N. Constant time per page, stable under inserts. Encode the cursor as base64 of a small JSON object so clients treat it as opaque. Discuss edge cases: ties on the sort key, bidirectional paging.
Reference implementation (typescript)
type Cursor = { createdAt: string; id: string };
export function decodeCursor(token: string | null): Cursor | null {
if (!token) return null;
return JSON.parse(Buffer.from(token, "base64url").toString());
}
export function encodeCursor(c: Cursor): string {
return Buffer.from(JSON.stringify(c)).toString("base64url");
}
export async function fetchFeed(after: string | null, limit = 20) {
const cursor = decodeCursor(after);
const rows = await db.query(
`SELECT id, created_at, body FROM posts
WHERE ($1::timestamptz IS NULL OR (created_at, id) < ($1, $2))
ORDER BY created_at DESC, id DESC LIMIT $3`,
[cursor?.createdAt ?? null, cursor?.id ?? null, limit],
);
const last = rows[rows.length - 1];
return {
items: rows,
nextCursor: last ? encodeCursor({ createdAt: last.created_at, id: last.id }) : null,
};
}Expect these follow-ups
- How do you implement bidirectional pagination (back and forth)?
- What if the sort field is a non-unique field like score?
- When is offset still the right choice?