7 lessons

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.

RequirementConsumersResource ModelStyleContractValidationAuthorizationErrorsIdempotencyPaginationVersioningObservabilityEvolutionTrade-offs

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.

Pagination: Choosing How Lists End
▶ lab

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.

Q · How does a client traverse this collection, and what happens to its traversal when the collection changes underneath it?
Offset Pagination: Simple, Jumpable, and Lying Under Writes
▶ lab

`?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.

Q · What does "page 3" actually promise, and what does serving it cost when the collection is large and moving?
Cursor Pagination: An Opaque Bookmark, Not a Position
▶ lab

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.

Q · What exactly does the continuation token encode, what does it promise across time and deploys, and what happens when it is stale?
Filtering: An Allowlist With an Index Bill

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.

Q · Which subsets of this collection does the contract promise to serve efficiently — and which combinations did you just promise by accident?
Sorting: Determinism or Drift

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.

Q · Is every ordering this API offers total, deterministic, index-backed — and did the docs say which one applies when the client says nothing?
Search Is a Different Contract Than Filtering

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.

Q · Is this endpoint promising exact membership in a predicate, or ranked relevance to an intent — and does its contract (results, pagination, consistency) match the promise?
Unbounded Collections: The Anti-Pattern With a Fuse

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.

Q · What is the largest response this endpoint can produce — and did you choose that number, or is it whatever the table holds that day?