Never return unbounded lists. Even if there are only 10 rows today, there will be 10 million tomorrow. Pagination is non-optional.
Offset
GET /products?offset=40&limit=20
// SQL: SELECT * FROM products LIMIT 20 OFFSET 40
{ "data": [...], "pagination": { "total": 1250, "offset": 40, "limit": 20 } }Cursor (recommended)
GET /products?limit=20
// Response:
{ "data": [...], "pagination": { "next_cursor": "eyJpZCI6MTIzfQ==", "has_more": true } }
// Next request:
GET /products?after=eyJpZCI6MTIzfQ==&limit=20Keyset
GET /products?after_id=123&limit=20
// SQL: WHERE id > 123 ORDER BY id ASC LIMIT 20Strategy comparison
| Strategy | Performance | Stable under inserts? | Random access | Best for |
|---|---|---|---|---|
| Offset | Poor (table scan) | No | Yes | Small datasets |
| Cursor | Excellent | Yes | No | Infinite scroll, feeds |
| Keyset | Excellent | Yes | No | Time-series, append-only |