Pagination
Pagination
Guide to paginating large datasets.
Table of Contents
- Page Number-Based Pagination
- Limit/Offset-Based Pagination
- Cursor-Based Pagination
- Pagination Metadata
- Best Practices
Page Number-Based Pagination
PageNumberPagination
Traditional page number-based pagination.
use reinhardt::pagination::{PageNumberPagination, Page};
use reinhardt::{Query, Path};
#[derive(serde::Deserialize)]
struct PageQuery {
page: Option<usize>,
page_size: Option<usize>,
}
async fn list_users(
Query(query): Query<PageQuery>,
) -> reinhardt::Response {
let page = query.page.unwrap_or(1);
let page_size = query.page_size.unwrap_or(10);
let pagination = PageNumberPagination::new(page, page_size);
// Fetch data from database
let total_count = fetch_user_count().await;
let users = fetch_users_page(page, page_size).await;
let page_data = Page::new(users, page, total_count, page_size);
let num_pages = (total_count + page_size - 1) / page_size;
let metadata = PaginationMetadata {
count: total_count,
next: if page < num_pages {
Some(format!("/api/users?page={}", page + 1))
} else {
None
},
previous: if page > 1 {
Some(format!("/api/users?page={}", page - 1))
} else {
None
},
};
let response = PaginatedResponse::new(users, metadata);
reinhardt::Response::ok().with_json(&response).unwrap()
}Page Number Validation
use reinhardt::pagination::PageNumberPagination;
let pagination = PageNumberPagination::new(page, page_size)
.with_max_page_size(100); // Maximum 100 items
if let Err(e) = pagination.validate() {
return reinhardt::Response::bad_request()
.with_json(&serde_json::json!({
"error": "Invalid page parameters",
"details": e.to_string()
}))
.unwrap();
}Limit/Offset-Based Pagination
LimitOffsetPagination
Flexible limit/offset-based pagination.
use reinhardt::pagination::{LimitOffsetPagination, PaginatedResponse};
#[derive(serde::Deserialize)]
struct OffsetQuery {
limit: Option<usize>,
offset: Option<usize>,
}
async fn list_items(
Query(query): Query<OffsetQuery>,
) -> reinhardt::Response {
let limit = query.limit.unwrap_or(20).min(100);
let offset = query.offset.unwrap_or(0);
let pagination = LimitOffsetPagination::new(limit, offset);
let total_count = fetch_item_count().await;
let items = fetch_items_limit_offset(limit, offset).await;
let metadata = PaginationMetadata {
count: total_count,
next: if offset + limit < total_count {
Some(format!("/api/items?limit={}&offset={}", limit, offset + limit))
} else {
None
},
previous: if offset > 0 {
let prev_offset = (offset - limit).max(0);
Some(format!("/api/items?limit={}&offset={}", limit, prev_offset))
} else {
None
},
};
let response = PaginatedResponse::new(items, metadata);
reinhardt::Response::ok().with_json(&response).unwrap()
}Cursor-Based Pagination
CursorPagination
Best for infinite scroll or real-time feeds.
use reinhardt::pagination::{CursorPagination, Cursor};
#[derive(serde::Deserialize, Serialize)]
struct ItemCursor {
id: String,
created_at: chrono::DateTime<chrono::Utc>,
}
async fn feed(
Query(query): Query<serde_json::Value>,
) -> reinhardt::Response {
let cursor = query.get("cursor")
.and_then(|v| v.as_str())
.map(|s| Cursor::new(s.to_string()));
let limit = query.get("limit")
.and_then(|v| v.as_u64())
.unwrap_or(20) as usize;
let pagination = CursorPagination::new(limit, cursor);
let items = fetch_items_cursor(pagination).await;
let next_cursor = items.last().map(|item: &Item| item.id.clone());
let has_more = items.len() == limit;
let response = serde_json::json!({
"results": items,
"next_cursor": next_cursor,
"has_more": has_more
});
reinhardt::Response::ok().with_json(&response).unwrap()
}Encoded Cursors
use reinhardt::pagination::{Cursor, CursorPagination};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
// Encode cursor
let cursor_data = format!("{}|{}", item_id, timestamp);
let encoded = URL_SAFE_NO_PAD.encode(cursor_data);
// Decode cursor
let decoded = URL_SAFE_NO_PAD.decode(encoded).unwrap();
let cursor_data = String::from_utf8(decoded).unwrap();Pagination Metadata
PaginatedResponse
Standard paginated response format.
use reinhardt::pagination::{PaginatedResponse, PaginationMetadata};
use serde::Serialize;
#[derive(Serialize)]
struct User {
id: u32,
username: String,
}
let response = PaginatedResponse {
count: 100,
next: Some("/api/users?page=2".to_string()),
previous: None,
results: users,
};
// JSON output:
// {
// "count": 100,
// "next": "/api/users?page=2",
// "previous": null,
// "results": [...]
// }Page Struct
Contains detailed page information.
use reinhardt::pagination::Page;
let page = Page::new(
items,
2, // Current page number
10, // Total pages
100, // Total items
10, // Page size
);
// Available information:
// - page.number: Current page number
// - page.num_pages: Total number of pages
// - page.count: Total items
// - page.start_index(): First item index
// - page.end_index(): Last item index
// - page.has_next(): Whether next page exists
// - page.has_previous(): Whether previous page existsBest Practices
Default Page Size
const DEFAULT_PAGE_SIZE: usize = 20;
const MAX_PAGE_SIZE: usize = 100;
let page_size = query.page_size
.unwrap_or(DEFAULT_PAGE_SIZE)
.min(MAX_PAGE_SIZE);Caching Total Count
use std::sync::Arc;
use tokio::sync::RwLock;
let count_cache = Arc::new(RwLock::new(HashMap::new()));
async fn get_count_with_cache(key: &str) -> usize {
// Check cache
{
let cache = count_cache.read().await;
if let Some(&count) = cache.get(key) {
return count;
}
}
// Fetch from database
let count = fetch_count_from_db().await;
// Store in cache
let mut cache = count_cache.write().await;
cache.insert(key.to_string(), count);
count
}Efficient Queries
use reinhardt::query::prelude::{Query, Postgres};
// Efficient pagination query (PostgreSQL)
let (limit, offset) = (page_size, (page - 1) * page_size);
let query = Query::select()
.columns([
User::Id,
User::Username,
User::Email,
])
.from(User::Table)
.order_by(User::CreatedAt, Order::Desc)
.limit(limit as u64)
.offset(offset as u64)
.to_owned();
// Separate query for total count
let count_query = Query::select()
.expr(Func::count(Expr::col(User::Id)))
.from(User::Table)
.to_owned();