Pagination
How to page through large Google Sheet datasets using SheetsAPI limit and offset parameters - cursor patterns, fetching all rows, and UI pagination.
SheetsAPI returns results in pages. Use the limit and offset query parameters to
control which rows you receive.
Parameters
| Parameter | Type | Default | Maximum | Description |
|---|---|---|---|---|
limit | integer | 100 | 500 | Number of rows to return |
offset | integer | 0 | - | Number of rows to skip |
Response meta object
Every response includes a meta block with the counts needed to drive pagination:
{
"data": [ ... ],
"meta": {
"total": 1234,
"limit": 100,
"offset": 0
}
}| Field | Meaning |
|---|---|
total | Total number of matching rows (respects search filters) |
limit | The limit value that was applied |
offset | The offset value that was applied |
Page 1
curl "https://sheetsapi.gkit.mreshank.com/api/spreadsheets/YOUR_USER_KEY/Products?limit=20&offset=0" \
-H "Authorization: Bearer sk_your_api_key_here"Page N
To fetch page n (1-based) with pageSize rows per page:
offset = (n - 1) * pageSize
# Page 3, 20 rows per page → offset = 40
curl "...?limit=20&offset=40"JavaScript pagination helper
async function getPage<T>(
sheet: string,
page: number,
pageSize: number,
params: Record<string, string> = {},
): Promise<{ rows: T[]; totalPages: number; total: number }> {
const url = new URL(`https://sheetsapi.gkit.mreshank.com/api/spreadsheets/${USER_KEY}/${sheet}`);
url.searchParams.set("limit", String(pageSize));
url.searchParams.set("offset", String((page - 1) * pageSize));
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const resp = await fetch(url, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
if (!resp.ok) throw new Error(`SheetsAPI ${resp.status}`);
const { data, meta } = await resp.json();
return {
rows: data as T[],
totalPages: Math.ceil(meta.total / pageSize),
total: meta.total,
};
}Fetching all rows
For small-to-medium sheets (up to ~5 000 rows), fetch all rows by looping until the
accumulated count equals meta.total:
async function getAllRows<T>(sheet: string): Promise<T[]> {
const all: T[] = [];
let offset = 0;
while (true) {
const resp = await fetch(
`https://sheetsapi.gkit.mreshank.com/api/spreadsheets/${USER_KEY}/${sheet}?limit=500&offset=${offset}`,
{ headers: { Authorization: `Bearer ${API_KEY}` } },
);
const { data, meta } = await resp.json();
all.push(...data);
offset += 500;
if (all.length >= meta.total) break;
}
return all as T[];
}limit=500 (the maximum) minimises the number of requests for large sheets.
Pagination with search and sort
limit and offset apply after filtering and sorting:
# Page 2 of products matching "category:books", sorted by price
curl "...?search=category:books&sort=-price&limit=20&offset=20"meta.total reflects the number of filtered rows, not the full sheet size. Use it to
calculate the correct total page count.
Cursor vs offset
SheetsAPI uses offset-based pagination. Offset is simple and works well when:
- Users navigate sequentially (first, previous, next, last)
- The sheet is not modified while the user is browsing
Limitations of offset pagination:
- Rows inserted at the top while paginating can cause duplicates or skips
- Large offsets scan all preceding rows and are slightly slower
For append-only sheets (new rows always go to the bottom) offset pagination is reliable and efficient.
UI patterns
React page controls
function Pagination({
page,
totalPages,
onChange,
}: {
page: number;
totalPages: number;
onChange: (p: number) => void;
}) {
return (
<nav aria-label="Pagination">
<button onClick={() => onChange(page - 1)} disabled={page <= 1}>
← Previous
</button>
<span>
Page {page} of {totalPages}
</span>
<button onClick={() => onChange(page + 1)} disabled={page >= totalPages}>
Next →
</button>
</nav>
);
}Next.js search params
// app/products/page.tsx (Server Component)
export default async function Page({ searchParams }: { searchParams: { page?: string } }) {
const page = parseInt(searchParams.page ?? "1");
const pageSize = 20;
const { data, meta } = await fetch(
`https://sheetsapi.gkit.mreshank.com/api/spreadsheets/${USER_KEY}/Products` +
`?limit=${pageSize}&offset=${(page - 1) * pageSize}`,
{
headers: { Authorization: `Bearer ${API_KEY}` },
next: { revalidate: 60 },
},
).then((r) => r.json());
const totalPages = Math.ceil(meta.total / pageSize);
return (
<>
<ul>
{data.map((p: { name: string }) => (
<li key={p.name}>{p.name}</li>
))}
</ul>
<a href={`?page=${page - 1}`} aria-disabled={page <= 1}>
Previous
</a>
<a href={`?page=${page + 1}`} aria-disabled={page >= totalPages}>
Next
</a>
</>
);
}Performance tips
- Use the largest
limityour UI can handle (up to 500) to minimise round trips - Cache paginated responses for read-heavy pages - see Caching Strategies
- Use
fieldsto return only the columns your UI needs, reducing response size:
curl "...?limit=100&fields=id,name,price"