Pagination & cursor helpers
List endpoints need a way to page through results that is stable under concurrent writes, cheap on the database, and self-describing in the contract. As of 0.37.0 DaloyJS ships built-in, dependency-free cursor-pagination helpers that cover all three concerns: an opaque cursor codec, an RFC 8288 Link header builder, and a paginationQuery() Standard Schema that validates the cursor / limit query parameters and wires them into the generated OpenAPI document and typed client.
Everything is built on Web-standard URL, btoa / atob, and JSON, so it runs unchanged on Node, Bun, Deno, Cloudflare Workers, and Vercel Edge.
Quick start
Mount paginationQuery() as the route's request.query. The handler receives a fully typed, validated { limit, cursor }; build the next cursor from the last row and advertise it with a Link header.
Opaque cursors
encodeCursor() serializes any JSON-serializable value (typically the sort key of the last row) into a compact, URL-safe base64url token. decodeCursor() reverses it.
Decoding is hardened: the input is capped at MAX_CURSOR_LENGTH(4 KiB), malformed base64url and invalid JSON are rejected, and any __proto__ / constructor / prototype keys in the decoded graph are stripped (prototype-pollution defense). A tampered cursor surfaces as a 400 Bad Request, not a 500.
RFC 8288 Link header
buildPageLinks() clones the current request URL and swaps its cursor query parameter to produce next, prev, and first page URLs — preserving every other query parameter (filters, limit, …) — then serializes them into a single Link header.
Need lower-level control? buildLinkHeader() serializes an explicit list of { url, rel, title? } entries. Both builders reject control characters, </> in URLs, and "/\\ in rel/title values — a structural defense against Link-header / response-splitting injection.
OpenAPI parameter wiring
Because paginationQuery() exposes a toJSONSchema() method, the OpenAPI generator emits the cursor and limit query parameters into the contract automatically — no duplicate parameter declarations, and the typed client picks them up on the next pnpm gen.
At runtime the same schema coerces limit from its string query value to an integer, clamps it to [minLimit, maxLimit], and rejects out-of-range or non-integer values at the request boundary with a 422. Customize the parameter names and bounds:
Security notes
- Cursors are opaque, not secret: they are encoded, not encrypted or signed. Never trust a decoded cursor for authorization — always re-scope the underlying query by the authenticated principal on the server.
decodeCursor()caps input length, rejects malformed tokens, and strips prototype-pollution keys, so a hostile cursor cannot crash the handler or poison object prototypes.- The
Linkbuilders reject CRLF, angle brackets, and quote characters, preventing header-injection through computed URLs or titles. maxLimitbounds the page size a client can request, protecting the database from unbounded scans.