Pagination
Reqon provides built-in support for the three most common pagination strategies: offset-based, page number-based, and cursor-based.
Pagination strategies
Offset-based pagination
Uses an offset value that increments by page size:
get "/users" {
paginate: offset(offset, 100),
until: length(response.data) == 0
}
How it works:
- First request:
?offset=0 - Second request:
?offset=100 - Third request:
?offset=200 - ...continues until
untilcondition is true
Parameters:
offset- Query parameter name for the offset value100- Page size (items per request)
Page number-based pagination
Uses a page number starting from 1:
get "/users" {
paginate: page(page, 50),
until: response.meta.hasNext == false
}
How it works:
- First request:
?page=1 - Second request:
?page=2 - Third request:
?page=3 - ...continues until
untilcondition is true
Parameters:
page- Query parameter name50- Page size
Cursor-based pagination
Uses a cursor token from the previous response:
get "/users" {
paginate: cursor(cursor, 100, "meta.nextCursor"),
until: response.meta.nextCursor == null
}
How it works:
- First request: no cursor parameter
- Response:
{ data: [...], meta: { nextCursor: "abc123" } } - Second request:
?cursor=abc123 - ...continues until cursor is null
Parameters:
cursor- Query parameter name100- Page size"meta.nextCursor"- Path to next cursor in response
Termination conditions
The until option specifies when to stop paginating:
Empty response
get "/items" {
paginate: offset(skip, 100),
until: length(response) == 0
}
Empty data array
get "/items" {
paginate: offset(skip, 100),
until: length(response.data) == 0
}
Boolean flag
get "/items" {
paginate: page(p, 50),
until: response.hasMore == false
}
// Or
get "/items" {
paginate: page(p, 50),
until: response.pagination.hasNext == false
}
Null cursor
get "/items" {
paginate: cursor(after, 100, "cursor.next"),
until: response.cursor.next == null
}
Maximum pages
get "/items" {
paginate: page(p, 100),
until: length(response.items) == 0 or p > 10
}
Item count threshold
get "/items" {
paginate: offset(skip, 100),
until: length(response) < 100 // Less than full page
}
Combining with other options
With query parameters
get "/users" {
params: {
status: "active",
sort: "created_at"
},
paginate: offset(offset, 100),
until: length(response.users) == 0
}
With retry
get "/users" {
paginate: cursor(cursor, 100, "nextCursor"),
until: response.nextCursor == null,
retry: {
maxAttempts: 3,
backoff: exponential
}
}
With incremental sync
get "/users" {
paginate: page(page, 100),
until: response.hasMore == false,
since: lastSync
}
Processing paginated results
Accumulative processing
All pages are accumulated in response:
action FetchAll {
get "/items" {
paginate: offset(offset, 100),
until: length(response) == 0
}
// response now contains ALL items from all pages
for item in response {
store item -> items { key: .id }
}
}
Per-page processing
Process each page as it arrives:
action ProcessPages {
get "/items" {
paginate: offset(offset, 100),
until: length(response) == 0
}
// For each page fetched, items are accumulated
// After pagination completes, process all
for item in response {
// Each item is processed
store item -> items { key: .id }
}
}
Common API patterns
Standard REST API
// API: GET /api/users?limit=100&offset=0
get "/api/users" {
params: { limit: 100 },
paginate: offset(offset, 100),
until: length(response.data) == 0
}
GraphQL-style cursor
// API uses cursor-based pagination
get "/api/items" {
paginate: cursor(after, 50, "pageInfo.endCursor"),
until: response.pageInfo.hasNextPage == false
}
Link header pagination
For APIs using Link headers, use cursor pagination:
get "/api/items" {
paginate: cursor(page, 100, "links.next"),
until: response.links.next == null
}
Best practices
Choose the right strategy
| API Type | Recommended Strategy |
|---|---|
| Stable datasets | Offset or Page |
| Real-time data | Cursor |
| Large datasets | Cursor |
| Simple APIs | Page |
Handle partial pages
get "/items" {
paginate: offset(offset, 100),
until: length(response.items) < 100 // Partial page = last page
}
Set reasonable page sizes
// Good: reasonable page size
get "/items" {
paginate: offset(offset, 100),
until: length(response) == 0
}
// Avoid: too large (may timeout or exceed limits)
get "/items" {
paginate: offset(offset, 10000),
until: length(response) == 0
}
// Avoid: too small (too many requests)
get "/items" {
paginate: offset(offset, 10),
until: length(response) == 0
}
Add safety limits
get "/items" {
paginate: page(page, 100),
until: length(response.items) == 0 or page > 100 // Max 100 pages
}
Combine with rate limiting
source API {
auth: bearer,
base: "https://api.example.com",
rateLimit: {
requestsPerMinute: 60,
strategy: "pause"
}
}
action FetchAll {
get "/items" {
paginate: offset(offset, 100),
until: length(response) == 0
}
}
Troubleshooting
Infinite pagination loop
If pagination never stops:
// Add a safety limit
get "/items" {
paginate: page(page, 100),
until: length(response.data) == 0 or page > 50 // Stop after 50 pages
}
Duplicate items
Some APIs return overlapping results. Use upsert:
get "/items" {
paginate: cursor(cursor, 100, "next"),
until: response.next == null
}
for item in response {
store item -> items { key: .id, upsert: true }
}
Missing items
If items are being missed, check your termination condition:
// May miss items if last page has exactly 100 items
until: length(response.items) == 0
// Better: check for less than full page
until: length(response.items) < 100