paginator-rs
Modular Rust pagination library with database and web framework integrations.
Features
- Page-based, offset/limit, cursor (keyset), and relative cursor pagination
- Builder API with multi-field sorting
- Filtering with 14 operators (eq, ne, gt, lt, gte, lte, like, ilike, in, between, is_null, is_not_null) and multi-field search
- Optional
COUNT(*)skipping via.disable_total_count() - Parameterized queries in all database integrations
- Serde serialization built in
Crates
| Crate | Purpose |
|---|---|
paginator-rs |
Core trait and types |
paginator-utils |
Shared types (params, response, metadata) |
paginator-sqlx |
SQLx (PostgreSQL, MySQL, SQLite) |
paginator-sea-orm |
SeaORM |
paginator-surrealdb |
SurrealDB |
paginator-axum |
Axum extractors and responses |
paginator-rocket |
Rocket guards and responders |
paginator-actix |
Actix-web extractors and responders |
paginator-zod |
zod-rs validation of pagination input and TypeScript response schemas |
Installation
[]
= "0.4.0"
Add the integration crate you need, for example:
= { = "0.4.0", = ["postgres", "runtime-tokio"] }
Usage
Building parameters
use ;
let params = new
.page
.per_page
.filter_eq
.filter_gt
.search
.sort_by
.sort_desc
.build;
Cursor pagination
Cursor (keyset) pagination orders rows by the cursor field and selects the rows on the far side of the cursor, so pages stay stable while rows are inserted or deleted. Every database integration supports it.
use ;
// Rows after id 42, newest first: WHERE id < 42 ORDER BY id DESC LIMIT 21
let params = new
.per_page
.sort_by
.sort_desc
.cursor_after
.disable_total_count // skip COUNT(*)
.build;
The response carries next_cursor and prev_cursor, derived from the last and first row of the page. Feed one back with .cursor_from_encoded(cursor), or the cursor query parameter in the web integrations, to move on. cursor_before fetches the page that ends just before a row, so both directions work. Cursors are URL-safe Base64 and validated on decode.
To hand out the first cursor from an ordinary offset page, call .with_cursors("id") on the response:
let page =
.await?
.with_cursors;
// page.meta.next_cursor is set whenever there is a next page
Rules and edge cases:
sort_by, when set, must equal the cursor field.sort_directionapplies as usual and must be sent along with the cursor.- The cursor field should be unique, such as a primary key. Keyset pagination on a non-unique column skips rows that share the boundary value.
- Cursors are only emitted when the cursor field is present in the serialized row type, so select it.
totalandtotal_pagesstill describe the whole result set, not the rows past the cursor.
Relative cursor pagination
page combines with a cursor as an offset in pages relative to it, so a client can jump several pages ahead of (or behind) a known cursor without walking through them:
// The third page after id 42: WHERE id > 42 ORDER BY id LIMIT 21 OFFSET 40
let params = new
.per_page
.page
.cursor_after
.build;
The response's page echoes the relative page number, and its cursors point at that page's boundary rows.
SQLx
use PaginatorBuilder;
use paginate_query;
let params = new.page.per_page.build;
let result = .await?;
println!;
Axum
use ;
async
SeaORM, SurrealDB, Rocket, and Actix-web work the same way; paginator-examples has a runnable example for every feature and integration:
Validating input with zod-rs
paginator-zod validates raw pagination query JSON against a zod-rs schema before it becomes PaginationParams, with path-aware, localizable errors. It enforces per_page bounds and, optionally, allow-lists for sort and filter fields.
use PaginationSchema;
use json;
let schema = new
.max_per_page
.allowed_sort_fields
.allowed_filter_fields;
// Ok -> PaginationParams, ready to paginate
let params = schema.validate?;
// Err -> "per_page: Too big: expected number to have <= 100"
schema.validate.unwrap_err;
It also emits a Zod schema for the response envelope so frontends get typed, validated responses:
println!;
// export const paginated = <T extends z.ZodTypeAny>(item: T) =>
// z.object({ data: z.array(item), meta: PaginationMetaSchema });
Response format
Cursor pagination adds next_cursor/prev_cursor, each present only when that side has more rows; with disable_total_count(), total and total_pages are omitted. Web framework integrations also set X-Total-Count, X-Total-Pages, X-Current-Page, and X-Per-Page headers.
Query parameters
GET /api/users?page=1&per_page=10&filter=status:eq:active&filter=age:gt:18&search=developer&search_fields=title,bio&sort_by=created_at&sort_direction=desc
page— 1-indexed, default 1per_page— default 20, max 100sort_by/sort_direction— field andasc/descfilter—field:operator:value, repeatable (AND logic)search/search_fields— query text and comma-separated fieldscursor— anext_cursor/prev_cursorfrom a previous response; invalid cursors are rejected with 400
License
MIT © 2025 Maulana Sodiqin