Filtering, sorting and paging links
Query parameters for filters and sort order, why an unknown parameter should be an error, and Link headers that tell the client where the next page is.
After this lesson you can
- Design filter and sort query parameters with an explicit allowlist
- Explain why silently ignoring an unknown filter is dangerous
- Write an RFC 8288 Link header for paging
Filtering, sorting and paging all shape which members of a collection come back, so they live in the query string of the collection's URI.
GET /orders?status=open&createdAfter=2026-01-01T00:00:00Z&sort=-createdAt,total&limit=50
Filters
The simplest convention is one parameter per field, with equality as the
default: status=open. Ranges need a convention of their own, and two
are common — named parameters (createdAfter, minTotal) or an operator
in the name (total[gte]=100). Neither is a standard; choose one and use
it everywhere. For several values, either repeat the parameter
(status=open&status=paid) or use a comma list (status=open,paid) —
again, one rule for the whole API.
The important decision is what happens to a parameter the endpoint does not know. Ignoring it looks forgiving, but a typo then changes the meaning of the request without any error:
const orders = [ { id: 1, status: "open" }, { id: 2, status: "paid" }, { id: 3, status: "open" }, { id: 4, status: "cancelled" },];const FILTERS = ["status"]; function lenient(query) { return orders.filter((o) => FILTERS.every((f) => !(f in query) || o[f] === query[f])).map((o) => o.id);} function strict(query) { const unknown = Object.keys(query).filter((k) => !FILTERS.includes(k)).sort(); if (unknown.length) return { status: 400, unknown }; return { status: 200, ids: lenient(query) };} export function run() { const typo = { stauts: "open" }; return { lenientCorrect: lenient({ status: "open" }), lenientTypo: lenient(typo), strictTypo: strict(typo), };}A client that misspelled status asked for open orders and received all
of them — and a bulk action run on that result would touch everything.
Rejecting unknown parameters with a 400 that names them turns a silent
data bug into an obvious one.
Filtering on arbitrary fields is also an index question: every filter or sort you advertise is a query the database must answer quickly at full size, so the allowlist is a performance contract as much as an API one.
Sorting
A widespread convention is a sort parameter holding a comma-separated
list of fields, each optionally prefixed with - for descending:
sort=-createdAt,total. Three rules make it safe:
- Allowlist the fields. Never pass a client's string into an
ORDER BY; map known names to known columns. - Reject duplicates and unknowns instead of guessing.
- End with a unique field so the order is total. As the previous lesson showed, cursor pagination is wrong without it, and even offset pages can shuffle rows that tie.
Links to other pages
RFC 8288 defines the Link header: each link is a URI in angle brackets
followed by parameters, most importantly rel, the relation type. The
registered relations next, prev, first and last are exactly what
paging needs, and a client can follow rel="next" without knowing how
your cursors or offsets work.
function linkHeader(base, links) { return Object.entries(links) .filter(([, query]) => query !== null) .map(([rel, query]) => `<${base}?${new URLSearchParams(query)}>; rel="${rel}"`) .join(", ");} export function run() { const base = "https://api.example.com/orders"; return linkHeader(base, { first: { limit: "50" }, next: { limit: "50", after: "WyIyMDI2IiwxMl0" }, prev: null, });}Many APIs put the same links in the JSON body ("next": "...") as well,
since some clients find headers awkward to read. Either is fine; a link
the client can follow beats instructions it has to assemble.
Common mistakes
- Silently ignoring unknown query parameters, so a misspelled filter returns the whole collection.
- Interpolating the
sortvalue into SQL — an injection hole and an unindexed query in one. - Sorting without a unique final key, so rows with equal values swap places between requests.
- Different conventions on different endpoints (
status=a,bhere,status=a&status=bthere).
What interviewers ask
- How would you design filtering and sorting for
/orders? — query parameters on the collection, an allowlist of filterable and sortable fields, a documented range and multi-value convention, a unique tie-breaker in every sort. - Should an API ignore unknown query parameters? — rejecting them is safer: a typo otherwise widens the result without warning.
- How does the client find the next page? — follow a link: a
Link: <...>; rel="next"header (RFC 8288) or anextURL in the body.
Exercise · Pro
Parse a sort parameter
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade