Why paginate?
Pagination looks trivial until you do it well across a real stack. The same list of records gets sliced into pages on the server, filtered and sorted from query parameters, searched by a text box, and scrolled with a cursor on the client — and the moment two services are written in different languages, the rules start to drift. A filter that means one thing in Python means something subtly different in TypeScript; a cursor minted by one service can't be read by another.
paginate solves that by implementing the whole thing once, in a Rust core, and exposing it through thin native packages for each language.
What it does
- Pagination — offset (page/limit) and keyset (cursor) models, with the page metadata derived for you.
- Filtering — 20 operators (
eq,gte,contains,between,in,regex, …) with flatAND/ORlists and nestedAnd()/Or()groups. - Sorting — stable, multi-key, with per-key direction and null placement.
- Search — ranked full-text with token matching and trigram fuzzy scoring.
- Integrations — SQLAlchemy, Django, FastAPI (Python); Express, Prisma, Drizzle (TypeScript).
What makes it different
- One implementation, no drift. Every operator, the sort comparator, the search ranker, and the cursor codec exist exactly once. The packages delegate; they never re-implement. See architecture.
- Cross-language parity. Python and TypeScript return the same filtered / sorted / ranked order and byte-identical cursors, pinned by a frozen golden fixture asserted in CI. See parity.
- Native, but honest about it. The engine is Rust, shipped inside the wheel / npm
addon — no toolchain to install. It wins big on the resident
Datasetpipeline and the cursor codec; for one-shot in-memory ops over data your host already holds, the packages are typed, correct, and convenient rather than a raw speed play. The benchmarks are candid about where the boundary pays off. - Typed end to end. The spec / param / page shapes are generated from a single JSON Schema emitted by the core, so the Python dataclasses and TypeScript interfaces can't disagree with each other or with Rust.
When to reach for it
- A polyglot system where a Python backend and a TypeScript frontend (or service) must agree on filters, sort order, and cursors.
- An API that needs consistent, well-defined filtering / sorting / search semantics without hand-rolling them per endpoint.
- An in-memory dataset served by many paginated requests — an in-memory cache, a
config table, a search index — where a resident
Datasetamortizes the work. - Anywhere you want cursor pagination that survives a switch of implementation language.
When you might not need it
- A single-language app doing one trivial
LIMIT/OFFSETquery may not need a dedicated engine — though the framework integrations still save boilerplate. - Hot, one-shot filtering/sorting of a list your host language already holds in
memory is often fastest in the host's own array operations; reach for
paginatethere for consistency, and for the fusedDatasetpipeline when you query the same data repeatedly.
Next
- Choosing a package for your stack.
- Architecture — how the core and adapters fit together.