Every list endpoint eventually needs to answer more than "give me everything" — and the difference between an API that's pleasant to filter and one that isn't usually comes down to a handful of small, consistent conventions applied everywhere, instead of a different ad-hoc scheme per endpoint.
3 min read
GET /products returning every product is fine until there are ten
thousand of them and a client only wants electronics under $50, sorted by
price. Real list endpoints need filtering (narrowing results), sorting
(ordering them), and often field selection (choosing which fields come
back) — and the specific syntax an API picks for each of these is one of
the most direct things a consumer experiences constantly, endpoint after
endpoint.
GET /products?category=electronics&price_lt=50&in_stock=true
A common, readable convention: one query parameter per filterable field,
with an optional suffix for the comparison (_lt, _gt, _ne) when it's
not a plain equality check. The key design goal is consistency across
every endpoint — if price_lt means "less than" on /products, a client
should be able to assume age_lt means the same thing on /users, without
checking each endpoint's documentation individually. An API that uses
price_lt in one place and maxPrice in another forces a client to
memorize per-endpoint exceptions instead of learning one pattern once.
GET /products?sort=price (ascending, default)
GET /products?sort=-price (descending — the "-" prefix convention)
GET /products?sort=category,-price (sort by category, then price descending)
The - prefix for descending order is a widely recognized convention
(used by JSON:API and many real-world APIs) precisely because it's compact
and composable — supporting multiple sort keys is just a comma-separated
list, without needing separate sort_by and sort_direction parameters
that have to be kept in sync.
GET /products/5?fields=id,name,price
Returning every field of a resource by default is simple, but wasteful when a client (especially a mobile client on a slow connection) only needs three of twenty fields. Sparse fieldsets let a client opt into exactly what it needs — genuinely useful for large resources, though GraphQL (lessons 4–5) was built specifically because this pattern, layered onto REST as an afterthought, only partially solves the deeper problem of a client needing data shaped differently across several related resources at once.
GET /products?q=wireless+headphones
A dedicated q (or search) parameter, distinct from structured filters,
signals free-text search rather than an exact-match constraint — mixing
the two into one parameter (forcing a client to guess whether a value is a
filter or a search term) is a common source of confusing API behavior.
Structured filters and free-text search are different operations with
different semantics, and keeping them syntactically distinct communicates
that difference to whoever's reading the API for the first time.
None of these conventions are individually complicated — the actual value
is in applying the same one everywhere. A client that learns
?sort=-field once can apply that knowledge to every endpoint in a
well-designed API without reading documentation each time; a client facing
a different filtering syntax per endpoint has to re-learn the API's
conventions over and over, which is a much larger, cumulative cost than any
single endpoint's specific syntax choice.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why does consistency across endpoints matter more than which specific filtering syntax an API picks?
2. Why is the '-' prefix convention for descending sort (e.g. '?sort=-price') useful?
3. Why should free-text search use a separate parameter from structured filters?
API Design