Three query parameters cover almost every "can the API also..." request you will get: a filter, a sort and a field list. All three share one rule — the server owns an allowlist, and anything outside it is a 400. Passing req.query into a database query is how a filter becomes an injection (Injection), and how ?sort=password becomes an index scan on a column you never meant to expose. Express 5 24,430 also changed the default query parser to simple, Node's own querystring, so ?year[gte]=2010 arrives as the literal key 'year[gte]' rather than a nested object — a flat map with no prototype-pollution surface.
const SORTABLE = new Set(['title', 'year', 'rating', 'id']);
const FIELDS = new Set(['id', 'title', 'author', 'year', 'genre', 'rating', 'inStock']);
function parseSort(sort = 'id') { // "-year,title" -> [['year',-1],['title',1]]
return sort.split(',').map((raw) => {
const key = raw.startsWith('-') ? raw.slice(1) : raw;
if (!SORTABLE.has(key)) throw new AppError('invalid_sort', 400,
`Cannot sort by '${key}'`, { parameter: 'sort', allowed: [...SORTABLE] });
return [key, raw.startsWith('-') ? -1 : 1];
});
}
const project = (doc, fields) => fields ? Object.fromEntries(fields.split(',').map((f) => {
if (!FIELDS.has(f)) throw new AppError('invalid_fields', 400, `Unknown field '${f}'`);
return [f, doc[f]];
})) : doc;A leading - for descending is the JSON:API convention most APIs copied; it survives URL encoding and composes a multi-key sort into one parameter. Sparse fieldsets matter too: a list view needing four fields of forty is the difference between a 6 KB and a 60 KB response on a phone.
> GET /api/v1/books?genre=fantasy&year[gte]=2015&sort=-rating&fields=id,title,year,rating
< 200 OK
{"data":[{"id":"bk_019","title":"The Fifth Season","year":2015,"rating":4.3},
{"id":"bk_041","title":"Circe","year":2018,"rating":4.3},
{"id":"bk_018","title":"Piranesi","year":2020,"rating":4.2},
{"id":"bk_020","title":"The Obelisk Gate","year":2016,"rating":4.2}],
"page":{"number":1,"size":20,"total":4,"pages":1}}
> GET /api/v1/books?sort=pages
< 400 Bad Request
{"error":{"code":"invalid_sort","message":"Cannot sort by 'pages'",
"details":{"parameter":"sort","allowed":["title","year","rating","id"]}}}The allowlist pays for itself in that second exchange: a typo returns neither an arbitrarily ordered list nor a book with a missing key, but the parameter and the values that would have worked. Two cautions. Every sortable key needs an index behind it, so the allowlist is a promise about your database, and MongoDB adds the compound indexes that back these keys. And ?fields=id,title and ?fields=title,id are different cache entries for identical bytes, so sort the names before building a cache key or an ETag.