| name | pagination-implementation |
| description | Pagination patterns for LIST operations including offset/limit and token-based |
Pagination Patterns
Complete patterns for implementing pagination in LIST operations across all producer implementations.
🚨 CRITICAL: Two Mutually Exclusive Pagination Approaches
There are TWO different pagination approaches. YOU CANNOT MIX THEM:
- Offset/Limit Pagination (pageNumber + pageSize) → NO pageToken assignment
- Token-Based Pagination (pageToken) → NO offset/limit parameters
NEVER use both in the same implementation!
Approach 1: Offset/Limit Pagination (STANDARD)
Use this for APIs that support offset/limit query parameters.
This is the STANDARD pattern for most LIST operations:
async list(results: PagedResults<ResourceType>, organizationId: string): Promise<void> {
const params: Record<string, number> = {};
if (results.pageNumber && results.pageSize) {
params.offset = (results.pageNumber - 1) * results.pageSize;
params.limit = Math.min(Math.max(results.pageSize, 1), 1000);
} else {
params.offset = 0;
}
const response = await this.httpClient.get(
`/orgs/${organizationId}/resources`,
{ params }
);
if (!response.data || !Array.isArray(response.data.data)) {
throw new UnexpectedError('Invalid response format: expected data array');
}
results.items = response.data.data.map(toResource);
results.count = response.data.totalCount || 0;
}
Approach 2: Token-Based Pagination (CURSOR)
Use this ONLY for APIs that use cursor-based pagination with tokens.
async list(results: PagedResults<ResourceType>, organizationId: string): Promise<void> {
const params: Record<string, string> = {};
if (results.pageToken) {
params.pageToken = results.pageToken;
}
const response = await this.httpClient.get(
`/orgs/${organizationId}/resources`,
{ params }
);
if (!response.data || !Array.isArray(response.data.data)) {
throw new UnexpectedError('Invalid response format: expected data array');
}
results.items = response.data.data.map(toResource);
results.count = response.data.totalCount || ;
results. = response.[];
}
Mandatory Requirements
1. Offset Initialization
CRITICAL: The else clause with params.offset = 0 is MANDATORY:
if (results.pageNumber && results.pageSize) {
params.offset = (results.pageNumber - 1) * results.pageSize;
params.limit = Math.min(Math.max(results.pageSize, 1), 1000);
} else {
params.offset = 0;
}
if (results.pageNumber && results.pageSize) {
params.offset = (results.pageNumber - 1) * results.pageSize;
params.limit = Math.min(Math.max(results.pageSize, 1), 1000);
}
WHY: APIs may require offset parameter even for first page. Missing offset can cause request failures or incorrect results.
2. Response Validation
REQUIRED: Always validate response structure before mapping:
if (!response.data || !Array.isArray(response.data.data)) {
throw new UnexpectedError('Invalid response format: expected data array');
}
results.items = response.data.data.map(toResource);
results.items = response.data?.data?.map(toResource) || [];
WHY:
- Fails fast with clear error message
- Prevents silent failures with empty arrays
- Consistent error handling across all producers
- TypeScript type narrowing ensures array exists
3. Limit Enforcement
Enforce minimum and maximum limits:
params.limit = Math.min(Math.max(results.pageSize, 1), 1000);
WHY:
- Prevents requesting 0 items (invalid)
- Prevents overwhelming API with huge page sizes
- Respects API rate limits and best practices
Parameter Conversion
PagedResults to API Parameters
Standard conversion pattern:
const params: Record<string, number> = {};
if (results.pageNumber && results.pageSize) {
params.offset = (results.pageNumber - 1) * results.pageSize;
params.limit = Math.min(Math.max(results.pageSize, 1), 1000);
} else {
params.offset = 0;
}
Examples:
- pageNumber=1, pageSize=25 → offset=0, limit=25
- pageNumber=3, pageSize=50 → offset=100, limit=50
- pageNumber=5, pageSize=100 → offset=400, limit=100
- No pagination → offset=0, no limit
Response Mapping
Standard Response Structure
Most APIs return paginated responses in this format:
{
"data": [
{ "id": "1", "name": "Resource 1" },
{ "id": "2", "name": "Resource 2" }
],
"totalCount": 42,
"metadata": { ... }
}
Mapping to PagedResults (Offset/Limit)
results.items = response.data.data.map(toResource);
results.count = response.data.totalCount || 0;
Fields for Offset/Limit Pagination:
items: Mapped domain objects (not raw API data)
count: Total number of items across all pages
pageToken: NOT USED - left undefined
Mapping to PagedResults (Token-Based)
results.items = response.data.data.map(toResource);
results.count = response.data.totalCount || 0;
results.pageToken = response.headers['x-next-page-token'];
Fields for Token-Based Pagination:
items: Mapped domain objects (not raw API data)
count: Total count (may not be available in cursor-based pagination)
pageToken: Next page token from response headers
Choosing the Right Approach
Use Offset/Limit (Approach 1) when:
- API supports
offset and limit query parameters
- API returns
totalCount in response
- Need to jump to specific pages (e.g., page 5)
- Most common for REST APIs
Use Token-Based (Approach 2) when:
- API requires
pageToken parameter
- API returns next page token in headers or response body
- Data changes frequently (cursor prevents skipped/duplicate items)
- API documentation explicitly uses cursor-based pagination
NEVER:
- Mix both approaches in the same implementation
- Assign
pageToken when using offset/limit
- Use offset/limit when API requires tokens
Complete Examples
Example 1: Simple LIST
async listUsers(results: PagedResults<User>, organizationId: string): Promise<void> {
const params: Record<string, number> = {};
if (results.pageNumber && results.pageSize) {
params.offset = (results.pageNumber - 1) * results.pageSize;
params.limit = Math.min(Math.max(results.pageSize, 1), 1000);
} else {
params.offset = 0;
}
const response = await this.httpClient.get(`/orgs/${organizationId}/users`, { params });
if (!response.data || !Array.isArray(response.data.data)) {
throw new UnexpectedError('Invalid response format: expected data array');
}
results.items = response.data..(toUser);
results. = response.. || ;
}
Example 2: LIST with Nested Path
async listGroupUsers(
results: PagedResults<UserInfo>,
organizationId: string,
groupId: string
): Promise<void> {
const params: Record<string, number> = {};
if (results.pageNumber && results.pageSize) {
params.offset = (results.pageNumber - 1) * results.pageSize;
params.limit = Math.min(Math.max(results.pageSize, 1), 1000);
} else {
params.offset = 0;
}
const response = await this.httpClient.get(
`/orgs/${organizationId}/groups/${groupId}/users`,
{ params }
);
if (!response.data || !Array.isArray(response.data.data)) {
throw new UnexpectedError('Invalid response format: expected data array');
}
results. = response...(toUserInfo);
results. = response.. || ;
}
Example 3: LIST with Additional Filters
async searchResources(
results: PagedResults<Resource>,
organizationId: string,
filter?: string
): Promise<void> {
const params: Record<string, string | number> = {};
if (results.pageNumber && results.pageSize) {
params.offset = (results.pageNumber - 1) * results.pageSize;
params.limit = Math.min(Math.max(results.pageSize, 1), 1000);
} else {
params.offset = 0;
}
if (filter) {
params.q = filter;
}
const response = await this.httpClient.get(
`/orgs/${organizationId}/resources`,
{ params }
);
if (!response.data || !Array.isArray(response.data.)) {
();
}
results. = response...(toResource);
results. = response.. || ;
}
Common Mistakes
Mistake 1: Missing Offset Initialization
if (results.pageNumber && results.pageSize) {
params.offset = (results.pageNumber - 1) * results.pageSize;
params.limit = Math.min(Math.max(results.pageSize, 1), 1000);
}
Impact: API requests may fail or return unexpected results.
Mistake 2: Ternary Instead of Validation
results.items = response.data?.data?.map(toResource) || [];
Impact:
- Masks API errors
- Returns empty array for malformed responses
- Difficult to debug issues
- Inconsistent error handling
Mistake 3: No Limit Bounds
params.limit = results.pageSize;
Impact:
- Can request 0 items (invalid)
- Can overwhelm API with huge requests
- May hit API rate limits
Mistake 4: Mixing Pagination Approaches
async list(results: PagedResults<Resource>, orgId: string): Promise<void> {
const params: Record<string, number> = {};
if (results.pageNumber && results.pageSize) {
params.offset = (results.pageNumber - 1) * results.pageSize;
params.limit = Math.min(Math.max(results.pageSize, 1), 1000);
}
results.pageToken = response.headers['x-next-page-token'];
}
Impact:
- Confuses pagination approach
- PageToken is meaningless in offset/limit pagination
- Can cause unexpected behavior in calling code
Correct:
results.items = response.data.data.map(toResource);
results.count = response.data.totalCount || 0;
Mistake 5: Wrong totalCount Fallback
results.count = response.data.totalCount || response.data.data.length;
Impact:
- Shows wrong total on pagination UI
- First page of 10 items shows "10 total" when actually 1000+
- Breaks pagination controls
Correct:
results.count = response.data.totalCount || 0;
Validation Checklists
Offset/Limit Pagination Checklist
Before completing a LIST operation with offset/limit pagination:
Token-Based Pagination Checklist
Before completing a LIST operation with token-based pagination:
Integration with PagedResults
The PagedResults<T> type is provided by @zerobias-org/types-core-js:
import { PagedResults } from '@zerobias-org/types-core-js';
interface PagedResults<T> {
items: T[];
count: number;
pageNumber?: number;
pageSize?: number;
pageToken?: string;
}
Input Parameters (provided by caller):
pageNumber: Which page to retrieve (1-based) - Offset/Limit only
pageSize: How many items per page - Offset/Limit only
pageToken: Token from previous response - Token-Based only
Output Fields (set by LIST method):
items: Mapped domain objects for current page - Always set
count: Total number of items across all pages - Always set
pageToken: Token for next page - Token-Based only, NOT set for Offset/Limit
References
- Producer Implementation: implementation-core-rules skill
- Error Handling: error-handling skill
- Operation Patterns: operation-patterns skill
- Operation Engineer: @.claude/agents/operation-engineer.md