typescriptintermediate

Cursor-Based Pagination API

Implement cursor-based pagination with forward/backward navigation, consistent ordering, and link headers.

typescript
interface PaginationParams {
  cursor?: string;
  limit?: number;
  direction?: 'forward' | 'backward';
}

interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    hasNext: boolean;
    hasPrev: boolean;
    nextCursor: string | null;
    prevCursor: string | null;
    total?: number;
  };
}

function encodeCursor(id: string, sortValue: string | number): string {
  return Buffer.from(JSON.stringify({ id, sv: sortValue })).toString('base64url');
}

function decodeCursor(cursor: string): { id: string; sv: string | number } {
  return JSON.parse(Buffer.from(cursor, 'base64url').toString());
}

// Simulated data
const items = Array.from({ length: 100 }, (_, i) => ({
  id: `item-${String(i).padStart(3, '0')}`,
  name: `Item ${i}`,
  createdAt: new Date(2024, 0, 1 + i).toISOString(),
  score: Math.floor(Math.random() * 1000),
}));

function paginate<T extends { id: string; createdAt: string }>(
  allItems: T[],
  params: PaginationParams,
): PaginatedResponse<T> {
  const limit = Math.min(params.limit ?? 20, 100);
  const sorted = [...allItems].sort((a, b) =>
    b.createdAt.localeCompare(a.createdAt),
  );

  let startIdx = 0;
  if (params.cursor) {
    const { id } = decodeCursor(params.cursor);
    const idx = sorted.findIndex(item => item.id === id);
    if (idx >= 0) {
      startIdx = params.direction === 'backward'
        ? Math.max(0, idx - limit)
        : idx + 1;
    }
  }

  const page = sorted.slice(startIdx, startIdx + limit);
  const hasNext = startIdx + limit < sorted.length;
  const hasPrev = startIdx > 0;

  return {
    data: page,
    pagination: {
      hasNext,
      hasPrev,
      nextCursor: hasNext && page.length > 0
        ? encodeCursor(page[page.length - 1].id, page[page.length - 1].createdAt)
        : null,
      prevCursor: hasPrev && page.length > 0
        ? encodeCursor(page[0].id, page[0].createdAt)
        : null,
      total: sorted.length,
    },
  };
}

// Test
const page1 = paginate(items, { limit: 5 });
console.log('Page 1:', page1.data.map(i => i.id));
console.log('Cursor:', page1.pagination.nextCursor);

if (page1.pagination.nextCursor) {
  const page2 = paginate(items, { limit: 5, cursor: page1.pagination.nextCursor });
  console.log('Page 2:', page2.data.map(i => i.id));
}

Use Cases

  • REST API pagination
  • Database result pagination
  • Infinite scroll backends

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.