Pagination
Pagination is a technique for managing large datasets by breaking them into smaller, manageable chunks called "pages." Instead of loading all data at once, pagination allows you to retrieve data incrementally, improving performance and user experience.
liblab automatically handles pagination in generated SDKs, eliminating the need for manual query management. The SDK provides two simple patterns:
- Iterable pagination: Automatically iterate through all pages using a simple
forloop - Manual pagination: Fine-grained control with
.next()to fetch pages on demand
Pagination types
liblab supports two pagination strategies:
| Pagination Type | Languages | Description |
|---|---|---|
| Offset-Limit | TypeScript, Java, Python | Uses numeric offset and limit parameters to navigate through data. Simple and widely supported. |
| Cursor | TypeScript, Java, Python | Uses opaque cursor tokens to maintain position in dataset. Provides better consistency and performance for frequently-changing data. |
Use offset-limit pagination for general use cases where data is relatively stable. Use cursor pagination when you need consistent results as data changes, better performance with large datasets, or the ability to resume pagination from saved positions.
How pagination works
When you configure pagination for an endpoint in your OpenAPI spec or liblab config file, the generated SDK automatically:
- Manages pagination parameters: Handles
offset/limitorcursortokens internally - Detects end of data: Knows when there are no more pages to fetch
- Provides simple iteration: Exposes clean APIs for both automatic and manual pagination
- Handles errors: Manages failed requests gracefully
You configure pagination once, and the SDK handles all the complexity.
Configuring pagination
liblab offers two methods to configure pagination:
- Using the OpenAPI spec: Add the
x-liblab-paginationannotation directly to your spec endpoint - Using
liblab.config.json: Configure pagination in the config file without modifying your spec
Both methods achieve the same result. Choose the approach that fits your workflow.
For complete configuration options and step-by-step setup, see:
Offset-Limit pagination
Supported SDK languages:
| TypeScript / Javascript | Java / Kotlin | Python | C# | Go | PHP |
|---|---|---|---|---|---|
| ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
Offset-limit pagination uses two numeric parameters to navigate through data:
- Offset: The number of items to skip (starting position)
- Limit: The maximum number of items to return per page
Configuration example
paths:
/pets:
get:
operationId: listPets
x-liblab-pagination:
type: offsetLimit
inputFields:
- name: offset
in: query
type: offset
- name: limit
in: query
type: limit
resultsArray:
results: "$.pets"
Usage patterns
The SDK provides two ways to use offset-limit pagination:
Iterable pagination
Automatically iterate through all pages:
- TypeScript
- Python
- Java
const sdk = new YourSdk({ baseUrl: 'https://api.example.com' });
// Automatically iterate through all pages
for await (const page of sdk.pets.listPets({ limit: 20, offset: 0 })) {
console.log(`Page data:`, page.data);
}
sdk = YourSdk(base_url="https://api.example.com")
# Automatically iterate through all pages
for page in sdk.pets.list_pets(limit=20, offset=0):
print(f"Page data: {page.data}")
YourSdk sdk = YourSdk.builder()
.baseUrl("https://api.example.com")
.build();
ListPetsParameters parameters = ListPetsParameters.builder()
.limit(20L)
.offset(0L)
.build();
Stream<List<Pet>> pagesStream = sdk.pets().listPets(parameters);
Iterator<List<Pet>> iterator = pagesStream.iterator();
List<Pet> allPets = new ArrayList<>();
iterator.forEachRemaining(page -> {
System.out.println("Got " + page.size() + " pets");
allPets.addAll(page);
});
Manual pagination
Control when to fetch each page:
- TypeScript
- Python
- Java
const sdk = new YourSdk({ baseUrl: 'https://api.example.com' });
const pages = sdk.pets.listPets({ limit: 20, offset: 0 });
// Fetch first page
const page1 = await pages.next();
console.log('First page:', page1.value.data);
// Fetch second page
const page2 = await pages.next();
console.log('Second page:', page2.value.data);
sdk = YourSdk(base_url="https://api.example.com")
pages = sdk.pets.list_pets(limit=20, offset=0)
# Fetch first page
page1 = next(pages)
print(f"First page: {page1.data}")
# Fetch second page
page2 = next(pages)
print(f"Second page: {page2.data}")
YourSdk sdk = YourSdk.builder()
.baseUrl("https://api.example.com")
.build();
ListPetsParameters parameters = ListPetsParameters.builder()
.limit(20L)
.offset(0L)
.build();
Stream<List<Pet>> pagesStream = sdk.pets().listPets(parameters);
Iterator<List<Pet>> iterator = pagesStream.iterator();
// Fetch first page
if (iterator.hasNext()) {
List<Pet> page1 = iterator.next();
System.out.println("First page: " + page1.size() + " pets");
}
// Fetch second page
if (iterator.hasNext()) {
List<Pet> page2 = iterator.next();
System.out.println("Second page: " + page2.size() + " pets");
}
When to use offset-limit
Offset-limit pagination is ideal when:
- Your dataset is relatively stable (items aren't frequently added/removed)
- You need simple numeric page navigation
- You want broad language support (TypeScript, Java, Python)
- Performance with large offsets is acceptable
Cursor pagination
Supported SDK languages:
| TypeScript / Javascript | Java / Kotlin | Python | C# | Go | PHP |
|---|---|---|---|---|---|
| ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
Cursor pagination uses opaque tokens instead of numeric offsets to maintain position in a dataset. Each response includes a cursor token that points to the next page.
Benefits
- Consistency: No duplicates or missing items when data changes during iteration
- Performance: Maintains consistent speed regardless of position in dataset
- Resume capability: Save and resume pagination from any cursor position
- Reliability: Handles real-time data changes gracefully
Configuration example
paths:
/organizations:
get:
operationId: listOrganizations
x-liblab-pagination:
type: cursor
inputFields:
- name: page_token
in: query
type: cursor
resultsArray:
results: "$.items"
nextCursor: "$.next_page_token"
Usage patterns
Cursor pagination works identically to offset-limit from a code perspective:
Iterable pagination
- TypeScript
- Python
- Java
const sdk = new YourSdk({ baseUrl: 'https://api.example.com' });
// Automatically iterate through all pages using cursor tokens
for await (const page of sdk.organizations.listOrganizations()) {
console.log(`Got ${page.data.length} organizations`);
}
sdk = YourSdk(base_url="https://api.example.com")
# Automatically iterate through all pages using cursor tokens
for page in sdk.organizations.list_organizations(page_size=50):
print(f"Got {len(page.data)} organizations")
YourSdk sdk = YourSdk.builder()
.baseUrl("https://api.example.com")
.build();
ListOrganizationsParameters parameters = ListOrganizationsParameters.builder()
.pageSize(50L)
.build();
Stream<List<Organization>> pagesStream = sdk.organizations().listOrganizations(parameters);
Iterator<List<Organization>> iterator = pagesStream.iterator();
List<Organization> allOrgs = new ArrayList<>();
iterator.forEachRemaining(page -> {
System.out.println("Got " + page.size() + " organizations");
allOrgs.addAll(page);
});
System.out.println("Total: " + allOrgs.size());
Key points:
- The SDK automatically manages cursor tokens
- You never see or handle cursor values directly
- Pagination ends when the API returns no
next_page_token
Manual pagination
- TypeScript
- Python
- Java
const sdk = new YourSdk({ baseUrl: 'https://api.example.com' });
const pages = sdk.organizations.listOrganizations();
// Fetch first page
const page1 = await pages.next();
if (!page1.done) {
console.log('First page:', page1.value.data);
}
// Fetch second page
const page2 = await pages.next();
if (!page2.done) {
console.log('Second page:', page2.value.data);
}
sdk = YourSdk(base_url="https://api.example.com")
pages = sdk.organizations.list_organizations(page_size=50)
# Fetch first page
page1 = next(pages)
print(f"First page: {len(page1.data)} organizations")
# Fetch second page
page2 = next(pages)
print(f"Second page: {len(page2.data)} organizations")
YourSdk sdk = YourSdk.builder()
.baseUrl("https://api.example.com")
.build();
ListOrganizationsParameters parameters = ListOrganizationsParameters.builder()
.pageSize(50L)
.build();
Stream<List<Organization>> pagesStream = sdk.organizations().listOrganizations(parameters);
Iterator<List<Organization>> iterator = pagesStream.iterator();
// Fetch first page
if (iterator.hasNext()) {
List<Organization> page1 = iterator.next();
System.out.println("First page: " + page1.size() + " organizations");
}
// Fetch second page
if (iterator.hasNext()) {
List<Organization> page2 = iterator.next();
System.out.println("Second page: " + page2.size() + " organizations");
}
Resume pagination
A unique feature of cursor pagination is resuming from a saved position:
- TypeScript
- Python
- Java
const sdk = new YourSdk({ baseUrl: 'https://api.example.com' });
// Resume from a previously saved cursor token
const savedCursor = 'eyJsYXN0X2lkIjoxMjM0fQ==';
for await (const page of sdk.organizations.listOrganizations({
pageToken: savedCursor // Resume from here
})) {
console.log('Resumed page:', page.data);
}
sdk = YourSdk(base_url="https://api.example.com")
# Resume from a previously saved cursor token
saved_cursor = "eyJsYXN0X2lkIjoxMjM0fQ=="
for page in sdk.organizations.list_organizations(
page_size=50,
page_token=saved_cursor # Resume from here
):
print(f"Resumed page: {len(page.data)} organizations")
YourSdk sdk = YourSdk.builder()
.baseUrl("https://api.example.com")
.build();
// Resume from a previously saved cursor token
String savedCursor = "eyJsYXN0X2lkIjoxMjM0fQ==";
ListOrganizationsParameters parameters = ListOrganizationsParameters.builder()
.pageSize(50L)
.pageToken(savedCursor) // Resume from here
.build();
Stream<List<Organization>> pagesStream = sdk.organizations().listOrganizations(parameters);
Iterator<List<Organization>> iterator = pagesStream.iterator();
iterator.forEachRemaining(page -> {
System.out.println("Resumed page: " + page.size() + " organizations");
});
This enables:
- Pausing and resuming pagination across sessions
- Implementing checkpointing for long-running operations
- Graceful failure recovery
When to use cursor pagination
Cursor pagination is ideal when:
- Your dataset changes frequently (items added/removed in real-time)
- You need consistent results across pagination requests
- Performance at scale matters (large datasets with deep pagination)
- You want to implement resume/checkpoint functionality
- Your application requires reliable pagination
Best practices
Configuration
- Match your API's parameter names: Use the exact parameter names your API expects
- Set correct JSONPath: Ensure
resultsArray.resultspoints to the array in your response - Test pagination: Verify pagination works with your API before releasing
Usage
- Use iterable pagination by default: It's simpler and handles most use cases
- Use manual pagination for control: When you need to limit requests or implement custom logic
- Handle errors gracefully: Wrap pagination in try-catch blocks
- Consider page size: Balance between request count and data volume
Performance
- Choose appropriate page sizes: Too small = many requests, too large = slow responses
- Use cursor pagination for large datasets: Better performance than offset-limit with deep pagination
- Implement resume logic: Save cursor positions for long-running operations
Examples and tutorials
- Build a TypeScript SDK with Pagination: Complete step-by-step tutorial covering both offset-limit and cursor pagination
- Pagination configuration reference: Detailed documentation of all configuration options