A collection endpoint that returns every row in the table works fine in a demo and falls over in production. Real APIs let clients ask for exactly the slice of data they need. In this lesson we design the query-parameter conventions for filtering, sorting, and pagination that power every serious list endpoint.
Everything after the ? in a URL is the query string: key-value pairs that refine a request without changing which resource it addresses.
GET /api/products?category=audio&sort=-price&page=2&limit=20Path segments identify the resource (the products collection); query parameters describe how to view it. Options, filters, and view settings belong in the query string, never in the path.
The simplest convention maps field names directly to equality filters: ?status=active&category=audio. For richer comparisons, common patterns include suffixed keys like ?price_gte=100&price_lte=500 (greater/less than or equal), or bracket syntax like ?price[gte]=100. Search across text fields usually gets its own parameter, conventionally ?q=wireless.
Whichever style you choose, validate every filter against an allow-list of filterable fields. Passing arbitrary parameters straight into database queries is both a performance trap and an injection risk.
A single sort parameter with a direction prefix has become the de facto standard: ?sort=price ascending, ?sort=-price descending, and comma-separated for tie-breaking: ?sort=-createdAt,name. Always define a default sort (often newest first) so results are deterministic, and reject unknown sort fields with 400 rather than silently ignoring them.
Offset pagination uses page and limit (or offset and limit):
GET /api/orders?page=3&limit=25The server skips (page - 1) * limit rows and returns the next batch, plus metadata:
{
"data": ["..."],
"meta": { "page": 3, "limit": 25, "total": 1240, "totalPages": 50 }
}Offset pagination is easy to implement and lets users jump to page 40 directly. Its weaknesses: deep pages get slow (the database still scans skipped rows), and results shift if rows are inserted or deleted between requests, causing duplicates or gaps.
Cursor (keyset) pagination fixes both problems. Instead of a page number, the server returns an opaque cursor pointing at the last item delivered; the client sends it back to continue.
GET /api/orders?limit=25&cursor=eyJpZCI6OTgxfQUnder the hood the cursor encodes a sort key like the last seen ID, and the query becomes WHERE id < 981 ORDER BY id DESC LIMIT 25, which stays fast at any depth and is immune to insertions shifting the window. The trade-off: no jumping to an arbitrary page. Twitter, Stripe, and Slack all paginate this way. Rule of thumb: cursor pagination for infinite feeds and large datasets, offset pagination for admin tables where page jumping matters.
Always enforce a default limit (say 20) and a hard maximum (say 100). Without a cap, one careless ?limit=1000000 request can take down your database. Return 400 for negative or non-numeric values rather than guessing.
Next up: Designing a Complete CRUD API, where all these pieces click together.