Pepelen
← Systems Analysis: APIs, Integrations and Data

Lesson

Lists and API evolution: pagination, filters, versions

Design a list endpoint with filters, sorting and pagination, and tell a contract change that is compatible with clients from a breaking one that requires a new version.

1 / 7

Long lists and the long life of an API

Long lists and the long life of an API

Thousands of orders don't go out in one response: the list is cut into pages. Offset/limit (?limit=20&offset=40) is simple and lets you jump to any page. But new records between requests shift the rows: the next page gets duplicates or gaps, and a big offset is costly for the database. Cursor (?limit=20&cursor=…): the server returns nextCursor, a marker like “after order 103”, and the next page starts after it. New records don't shift pages and the query is fast, but you can't jump to page 37. Feeds and exports use a cursor. Always set a limit cap (100 at Romashka) and an explicit sort, or pages are unstable. Filters and sorting are parameters too: ?status=delivered&createdFrom=2026-10-01&sort=-createdAt is delivered orders since October 1, the minus meaning “descending”. The response is {"items": […], "nextCursor": "…"}; no nextCursor, no more pages. Clients don't update at once. They are “tolerant readers” that ignore unknown fields, so new optional fields, parameters and endpoints are compatible. A breaking change ships only in a new version (/v2); the old one is supported until an announced date, flagged by the Deprecation (RFC 9745) and Sunset (RFC 8594) headers. A special case: a new enum value in a response breaks clients not ready for unknown values. So the contract says up front what the client does with an unknown status, and new values are announced early.
Breaking or not
ChangeCompatible?Why
a new optional field in the responseyesold clients don't read it
a new optional query parameteryeswithout it, the behavior stays the same
a new endpointyesexisting calls don't change
remove or rename a fieldnoclients look for the old name
change a field's type, e.g. number → stringnoparsing the response fails
make a request field requirednoold requests without the field get an error
change the meaning of a code or statusnothe client reacts the old way
Lesson notes
Long lists and the long life of an API
Thousands of orders don't go out in one response: the list is cut into pages. Offset/limit (?limit=20&offset=40) is simple and lets you jump to any page. But new records between requests shift the rows: the next page gets duplicates or gaps, and a big offset is costly for the database. Cursor (?limit=20&cursor=…): the server returns nextCursor, a marker like “after order 103”, and the next page starts after it. New records don't shift pages and the query is fast, but you can't jump to page 37. Feeds and exports use a cursor. Always set a limit cap (100 at Romashka) and an explicit sort, or pages are unstable. Filters and sorting are parameters too: ?status=delivered&createdFrom=2026-10-01&sort=-createdAt is delivered orders since October 1, the minus meaning “descending”. The response is {"items": […], "nextCursor": "…"}; no nextCursor, no more pages. Clients don't update at once. They are “tolerant readers” that ignore unknown fields, so new optional fields, parameters and endpoints are compatible. A breaking change ships only in a new version (/v2); the old one is supported until an announced date, flagged by the Deprecation (RFC 9745) and Sunset (RFC 8594) headers. A special case: a new enum value in a response breaks clients not ready for unknown values. So the contract says up front what the client does with an unknown status, and new values are announced early.