Skip to content

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():

python
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():

python
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:

json
{
  "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.

python
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.

python
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.

python
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:

python
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:

python
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 < 0
  • InvalidPageSizeError: Page size < 1 or limit < 0
  • InvalidCursorError: Malformed cursor encoding

Filtering and Sorting

Pagination works seamlessly with filtering and sorting:

python
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 count
  • get_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 count
  • async 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

python
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

python
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 request
  • calculate_offset_limit(*args) -> Tuple[int, int]: Convert parameters to offset/limit
  • generate_metadata(total_items, items, base_url, request_params) -> Dict[str, Any]: Create pagination metadata

Advanced Custom Strategy

python
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