Lists, filters and versions
🎯You'll be able to
Design list endpoints that page through large results, filter predictably and change over time without breaking callers.
🏠The everyday picture
A library catalogue doesn't hand you every book at once. It shows twenty, with "next page", and you can filter by author. When the library adds a new field to its cards, old searches still work.
📘What it really is
- Never return an unbounded list. Set a default and a maximum page size (
limit=20, at most 50). - Two ways to page:
- offset (
?offset=40&limit=20): simple, but slow on deep pages and items can shift between pages as new rows arrive; - cursor (
?after=<id or timestamp>): the server says "continue after this item"; stable and fast with an index, but you can't jump to page 37. -
Filters and sorting use query parameters with a short allowed list (
?status=delivered&sort=-placed_at); reject unknown ones with400rather than ignoring them silently. -
Versioning: adding a field is safe; removing or renaming a field, or changing its meaning, breaks callers. Put a version in the path (
/v1/) or a header, and keep the old version until callers have moved. -
Return IDs and timestamps in standard forms: ISO 8601 times with a zone (
2026-11-01T12:30:00Z), money as whole paise.
📝A worked example
A cursor design for GET /v1/me/orders?limit=2 (the kit itself only needs limit) returns:
{"orders": [{"id": 120044, "placed_at": "2026-11-01T07:12:00Z", "total_paise": 26000},
{"id": 119980, "placed_at": "2026-11-01T06:40:00Z", "total_paise": 13000}],
"next": "/v1/me/orders?limit=2&before=2026-11-01T06:40:00Z_119980"}
The cursor combines the time and the ID, so two orders placed in the same second still page correctly. The query behind it is
WHERE (placed_at, id) < ($1, $2) ORDER BY placed_at DESC, id DESC LIMIT $3, and an index on (customer_id, placed_at, id) keeps
it fast however deep the customer scrolls.
🌍Where it's used
- Order histories, feeds and statements in shopping, social and banking apps.
- Admin dashboards listing thousands of records.
- Public APIs that must not break partners when they grow.
📝Case scenario
An API renames total to total_paise in place, to make the unit clear. Overnight, a partner's invoice tool shows ₹0 on every
invoice because total is missing. The safer path: add total_paise alongside total, mark total as deprecated in the
documentation, tell callers, and remove it only in /v2/.
🖱️Try it
Add limit handling to the kit's GET /v1/me/orders (default 20, at most 50). Then sketch, on paper, the cursor version above.
Success: ?limit=500 returns at most 50 orders, and ?limit=-3 returns a sensible default.
⚠️Common mistakes
- No upper limit. Sign:
?limit=1000000takes the database down. Fix: clamp the page size on the server. - Offset paging on a busy, growing list. Sign: users see duplicates or miss items while scrolling. Fix: use a cursor based on a stable sort key.
- Breaking changes without a new version. Sign: renamed or removed fields break existing apps. Fix: add, deprecate, then remove in a new version.
🛠️For your project
Project 1 test 7 calls GET /v1/me/orders with extra parameters; your list must stay limited to the signed-in customer's orders.
✅Quick check
Why is a cursor more reliable than an offset for a feed that keeps growing?
Show the answer
New items shift every offset, so pages repeat or skip items; a cursor continues after a known item.
Is adding a new field to a response a breaking change?
Show the answer
Usually not: callers that don't know the field ignore it. Removing or renaming a field is.
