Why query design matters
Large datasets are inevitable. Without pagination and filtering, clients will attempt to fetch everything and your API will slow down or fail.
Clear query rules also protect your infrastructure and make client integrations much easier to maintain.
Offset pagination: simple but fragile
Offset pagination (`page` + `limit`) is easy to implement and easy to explain. It is a good default for small datasets.
The downside is instability: if records are inserted or deleted between requests, results shift and users can see duplicates or misses.
Cursor pagination: stable and scalable
Cursor pagination uses a pointer (cursor) to the last seen item. It remains stable even when data changes.
It is more complex for clients, but it is the better choice for large or fast-changing datasets.
Sorting should be explicit
Provide a `sort` parameter and document allowed fields. A simple convention is `sort=created_at` for ascending and `sort=-created_at` for descending.
Sorting must be deterministic; otherwise pagination breaks.
Filtering rules
Use query parameters for filters: `/orders?status=paid&customer_id=123`. This is flexible and easy to extend.
Avoid encoding filters in paths unless they represent distinct resource types.
Field selection
If clients only need a few fields, provide a `fields` parameter. It reduces bandwidth and improves performance.
Keep field selection controlled to avoid accidental exposure of sensitive data.
Metadata and totals
Clients often want total counts, but they can be expensive to compute. Provide them only when useful.
For cursor pagination, a `next_cursor` and `has_next` are often enough.
Error handling for queries
If a filter or sort is invalid, return a clear 400 with a message describing the issue.
Query rules are part of your API contract, so document them and keep them stable.
