📄 Pagination
Nexios provides a powerful, flexible pagination system that supports multiple strategies, custom data handlers, and seamless integration with your existing APIs. Whether you're building simple list pagination or complex cursor-based navigation, Nexios has you covered.
🚀 Quick Start
Here's a quick example of how to paginate a list of items using response.paginate():
from nexios import NexiosApp
app = NexiosApp()
@app.get("/get-items")
async def get_items(request, response):
sample_data = [{"id": i, "content": f"Item {i}"} for i in range(1, 101)]
return response.paginate(sample_data)For async endpoints, use response.apaginate():
from nexios import NexiosApp
app = NexiosApp()
@app.get("/get-items")
async def get_items(request, response):
sample_data = [{"id": i, "content": f"Item {i}"} for i in range(1, 101)]
return response.apaginate(sample_data)📊 Response Format
The pagination response includes both the data and comprehensive metadata:
{
"items": [
{"id": 1, "content": "Item 1"},
{"id": 2, "content": "Item 2"},
"...more items..."
],
"pagination": {
"total_items": 100,
"total_pages": 5,
"page": 1,
"page_size": 20,
"links": {
"next": "http://127.0.0.1:8000/items?page=2&page_size=20",
"first": "http://127.0.0.1:8000/items?page=1&page_size=20",
"last": "http://127.0.0.1:8000/items?page=5&page_size=20"
}
}
}💡 Navigation Links
The links section provides ready-to-use URLs for navigating between pages. The prev link appears only when not on the first page.
🗂️ Pagination Strategies
Nexios supports three main pagination strategies, each suited for different use cases:
1. Page Number Pagination (Default)
The most common pagination style, using page numbers and page sizes. Perfect for traditional web applications.
from nexios import NexiosApp
app = NexiosApp()
@app.get("/get-items")
async def get_items(request, response):
sample_data = [{"id": i, "content": f"Item {i}"} for i in range(1, 101)]
return response.paginate(sample_data, strategy="page_number")Parameters:
page_param: Query parameter name for page number (default: "page")page_size_param: Query parameter name for page size (default: "page_size")default_page: Default page number (default: 1)default_page_size: Default page size (default: 20)max_page_size: Maximum allowed page size (default: 100)
Example URLs:
/items?page=2&page_size=10- Page 2 with 10 items per page/items?page_size=50- First page with 50 items per page/items- Uses defaults (page 1, 20 items per page)
2. Limit-Offset Pagination
Traditional SQL-style pagination using limit and offset. Ideal for database queries and APIs that follow REST conventions.
from nexios import NexiosApp
app = NexiosApp()
@app.get("/get-items")
async def get_items(request, response):
sample_data = [{"id": i, "content": f"Item {i}"} for i in range(1, 101)]
return response.paginate(sample_data, strategy="limit_offset")Parameters:
limit_param: Query parameter name for limit (default: "limit")offset_param: Query parameter name for offset (default: "offset")default_limit: Default limit value (default: 20)max_limit: Maximum allowed limit value (default: 100)
Example URLs:
/items?limit=10&offset=20- Items 21-30/items?limit=50- First 50 items/items?offset=100- Items starting from 101 (uses default limit)
3. Cursor Pagination
Cursor-based pagination for consistent pagination with changing datasets. Perfect for real-time feeds, infinite scroll, and large datasets.
from nexios import NexiosApp
app = NexiosApp()
@app.get("/get-items")
async def get_items(request, response):
sample_data = [{"id": i, "content": f"Item {i}"} for i in range(1, 101)]
return response.paginate(sample_data, strategy="cursor")Parameters:
cursor_param: Query parameter name for cursor (default: "cursor")page_size_param: Query parameter name for page size (default: "page_size")default_page_size: Default page size (default: 20)max_page_size: Maximum allowed page size (default: 100)sort_field: Field to use for cursor sorting (default: "id")
Example URLs:
/items?cursor=eyJpZCI6IDEwfQ%3D%3D&page_size=10- Items after ID 10/items?page_size=50- First 50 items/items- Uses defaults (first page, 20 items per page)
💡 Cursor Encoding
Cursors are base64-encoded JSON objects containing the sort field value. This makes them URL-safe and opaque to clients.
🔧 Advanced Configuration
Custom Strategy Parameters
You can customize pagination strategies by passing strategy instances instead of strings:
from nexios import NexiosApp
from nexios.pagination import PageNumberPagination, CursorPagination
app = NexiosApp()
# Custom page number pagination
@app.get("/items-page")
async def get_items_page(request, response):
data = [{"id": i, "name": f"Item {i}"} for i in range(1, 101)]
strategy = PageNumberPagination(
page_param="p",
page_size_param="size",
default_page=1,
default_page_size=10,
max_page_size=50
)
return response.paginate(data, strategy=strategy)
# Custom cursor pagination with timestamp sorting
@app.get("/items-cursor")
async def get_items_cursor(request, response):
data = [
{"id": i, "name": f"Item {i}", "created_at": f"2023-01-{i:02d}T00:00:00Z"}
for i in range(1, 31)
]
strategy = CursorPagination(
sort_field="created_at",
default_page_size=5
)
return response.paginate(data, strategy=strategy)Error Handling
Nexios provides built-in error handling for invalid pagination parameters:
from nexios import NexiosApp
from nexios.pagination import InvalidPageError, InvalidPageSizeError, InvalidCursorError
from nexios.http import HTTPException
app = NexiosApp()
@app.get("/items")
async def get_items(request, response):
try:
data = [{"id": i, "name": f"Item {i}"} for i in range(1, 101)]
return response.paginate(data)
except InvalidPageError as e:
raise HTTPException(status_code=400, detail=str(e))
except InvalidPageSizeError as e:
raise HTTPException(status_code=400, detail=str(e))
except InvalidCursorError as e:
raise HTTPException(status_code=400, detail=str(e))Common Errors:
InvalidPageError: Page number < 1 or offset < 0InvalidPageSizeError: Page size < 1 or limit < 0InvalidCursorError: Malformed cursor encoding
Filtering and Sorting
Pagination works seamlessly with filtering and sorting:
from nexios import NexiosApp
app = NexiosApp()
@app.get("/products")
async def get_products(request, response):
# Sample data with categories
all_products = [
{"id": i, "name": f"Product {i}", "category": "electronics" if i % 2 == 0 else "books", "price": i * 10}
for i in range(1, 101)
]
# Apply filters
category = request.query_params.get("category")
min_price = request.query_params.get("min_price")
filtered_products = all_products
if category:
filtered_products = [p for p in filtered_products if p["category"] == category]
if min_price:
filtered_products = [p for p in filtered_products if p["price"] >= float(min_price)]
# Sort results
sort_by = request.query_params.get("sort", "id")
reverse = request.query_params.get("order") == "desc"
filtered_products.sort(key=lambda x: x.get(sort_by, 0), reverse=reverse)
return response.paginate(filtered_products)Example URLs:
/products?category=electronics&sort=price&order=desc/products?min_price=50&page=2&page_size=20/products?category=books&sort=name
💡 Preserving Query Parameters
Pagination links automatically preserve non-pagination query parameters, so filters and sorting persist across page navigation.
🔄 Data Handlers
Data handlers abstract the data source, allowing pagination to work with any type of data storage. Nexios provides built-in handlers for in-memory lists and async operations.
Built-in Data Handlers
SyncDataHandler
Base class for synchronous data handlers with two required methods:
get_total_items() -> int: Returns total item countget_items(offset: int, limit: int) -> List[Any]: Returns paginated items
Built-in Implementation:
SyncListDataHandler: Handles in-memory lists
AsyncDataHandler
Base class for asynchronous data handlers with two required methods:
async get_total_items() -> int: Returns total item countasync get_items(offset: int, limit: int) -> List[Any]: Returns paginated items
Built-in Implementation:
AsyncListDataHandler: Handles in-memory lists asynchronously
💡 Automatic Selection
By default, .paginate() uses AsyncListDataHandler for async functions and SyncListDataHandler for sync functions.
🏗️ Custom Data Handlers
Create custom data handlers to integrate with databases, external APIs, or any data source:
Database Integration Examples
Tortoise ORM Example
from nexios import NexiosApp
from nexios.pagination import AsyncDataHandler
from tortoise.models import Model
from .models import Item
app = NexiosApp()
class TortoiseDataHandler(AsyncDataHandler):
def __init__(self, query):
self.query = query
async def get_total_items(self) -> int:
return await self.query.count()
async def get_items(self, offset: int, limit: int) -> List[Any]:
return await self.query.offset(offset).limit(limit).all()
@app.get("/items")
async def get_items(request, response):
# Apply filters from query parameters
query = Item.all()
return response.paginate(query, data_handler=TortoiseDataHandler(query))⚙️ Custom Pagination Strategies
Create custom pagination strategies by subclassing BasePaginationStrategy:
Custom Strategy Example
from nexios import NexiosApp
from nexios.pagination import BasePaginationStrategy
from typing import Any, Dict, Tuple
app = NexiosApp()
class SeekPagination(BasePaginationStrategy):
"""Custom seek-based pagination similar to Facebook's API"""
def __init__(self, seek_param: str = "seek", page_size_param: str = "limit", default_page_size: int = 20):
self.seek_param = seek_param
self.page_size_param = page_size_param
self.default_page_size = default_page_size
def parse_parameters(self, request_params: Dict[str, Any]) -> Tuple[Optional[str], int]:
seek_value = request_params.get(self.seek_param)
page_size = int(request_params.get(self.page_size_param, self.default_page_size))
return seek_value, page_size
def calculate_offset_limit(self, seek_value: Optional[str], page_size: int) -> Tuple[int, int]:
# For seek pagination, we'd typically use this in the database query
# Here we simulate with offset calculation
if seek_value:
# In real implementation, you'd find the position of seek_value
return int(seek_value), page_size
return 0, page_size
def generate_metadata(self, total_items: int, items: List[Any],
base_url: str, request_params: Dict[str, Any]) -> Dict[str, Any]:
seek_value, page_size = self.parse_parameters(request_params)
# Generate next seek value
next_seek = str(int(seek_value or 0) + len(items)) if items else None
links = {}
if next_seek and len(items) == page_size:
links["next"] = f"{base_url}?{self.seek_param}={next_seek}&{self.page_size_param}={page_size}"
return {
"total_items": total_items,
"page_size": page_size,
"seek_value": seek_value,
"has_more": len(items) == page_size,
"links": links
}
@app.get("/items-seek")
async def get_items_seek(request, response):
data = [{"id": i, "name": f"Item {i}"} for i in range(1, 101)]
return response.paginate(data, strategy=SeekPagination())Overridable Methods
When creating custom strategies, you can override these methods:
parse_parameters(request_params: Dict[str, Any]) -> Any: Extract pagination parameters from requestcalculate_offset_limit(*args) -> Tuple[int, int]: Convert parameters to offset/limitgenerate_metadata(total_items, items, base_url, request_params) -> Dict[str, Any]: Create pagination metadata
Advanced Custom Strategy
from nexios.pagination import BasePaginationStrategy, LinkBuilder
from typing import Any, Dict, Tuple
import math
class HybridPagination(BasePaginationStrategy):
"""Hybrid strategy that supports both page and cursor-based pagination"""
def __init__(self, page_param: str = "page", cursor_param: str = "cursor",
page_size_param: str = "page_size", default_page_size: int = 20):
self.page_param = page_param
self.cursor_param = cursor_param
self.page_size_param = page_size_param
self.default_page_size = default_page_size
def parse_parameters(self, request_params: Dict[str, Any]) -> Dict[str, Any]:
page = request_params.get(self.page_param)
cursor = request_params.get(self.cursor_param)
page_size = int(request_params.get(self.page_size_param, self.default_page_size))
return {
"page": int(page) if page else None,
"cursor": cursor,
"page_size": page_size,
"is_cursor_based": cursor is not None
}
def calculate_offset_limit(self, params: Dict[str, Any]) -> Tuple[int, int]:
if params["is_cursor_based"]:
# Cursor-based logic (simplified)
return int(params["cursor"] or 0), params["page_size"]
else:
# Page-based logic
page = params["page"] or 1
return (page - 1) * params["page_size"], params["page_size"]
def generate_metadata(self, total_items: int, items: List[Any],
base_url: str, request_params: Dict[str, Any]) -> Dict[str, Any]:
params = self.parse_parameters(request_params)
metadata = {
"total_items": total_items,
"page_size": params["page_size"],
"strategy": "cursor" if params["is_cursor_based"] else "page"
}
if params["is_cursor_based"]:
# Cursor metadata
next_cursor = str(int(params["cursor"] or 0) + len(items)) if items else None
metadata.update({
"cursor": params["cursor"],
"next_cursor": next_cursor,
"has_more": len(items) == params["page_size"]
})
else:
# Page metadata
page = params["page"] or 1
total_pages = math.ceil(total_items / params["page_size"]) if params["page_size"] else 1
metadata.update({
"page": page,
"total_pages": total_pages,
"has_next": page < total_pages,
"has_prev": page > 1
})
return metadata