Pagination, Filtering & Search
Every list endpoint is a query API. Offset vs cursor under concurrent writes, stable ordering, filter allowlists, search as a different contract — and the index each promise requires.
Every lesson below names the consumers, the design question and the guarantee before recommending anything. Recommendations come with what they cost, when not to use them, and how they evolve.
Every list endpoint needs an answer to "and then what?" before the collection grows. Offset, cursor and keyset pagination are different promises about consistency, cost and navigation — and the consumer's access pattern picks, not fashion.
`?page=3&limit=50` is the easiest pagination to build and consume, and it makes two quiet promises it cannot keep at scale: that deep pages are as cheap as shallow ones, and that page boundaries hold still while the collection changes.
A cursor is the server saying "resume after this row" in a token the client stores but never reads. Done right it makes deep traversal flat-cost and write-stable; done lazily it leaks internals, breaks on deploys, and quietly becomes offset with extra steps.
Every filter parameter is a promise that a class of database queries will stay fast forever. Explicit, typed, allowlisted filters keep that promise affordable; a generic query language hands your query planner to strangers.
An ORDER BY in the contract is two promises: that the ordering is affordable, and that it is deterministic. Skip the tiebreaker and pagination corrupts; allowlist nothing and every column is an index you owe.
Filtering promises the exact subset matching a predicate; search promises the most *relevant* results for an expression of intent. Different guarantees, different cost model, different pagination — pretending one is the other breaks both.
GET /orders returning "all of them" works flawlessly until the collection grows — then it fails everywhere at once, and the fix is a breaking change to every consumer. The bound you did not design is the outage you scheduled.