A query string can repeat the same parameter. That is useful for a list of tags but confusing for a singleton such as a page number. Choosing the first value in one component and the last value in another can make request behavior inconsistent.
This example defines a small query contract: page appears at most once and defaults to one; tag may repeat up to five times. Unknown names are rejected. The parser uses URLSearchParams for decoding and then applies application-specific validation.
Prerequisites
Use a supported Node.js LTS release to run the fixture program. It was tested with Node.js v26.13.0. Save this code as query.mjs. The readQuery function uses the standard URLSearchParams interface; the test harness uses Node's built-in assertion module.
import assert from 'node:assert/strict';
function readQuery(search) {
const params = new URLSearchParams(search);
for (const key of params.keys()) {
if (!['page', 'tag'].includes(key)) throw new Error('unknown parameter');
}
const pages = params.getAll('page');
if (pages.length > 1) throw new Error('duplicate page');
const raw = pages[0] ?? '1';
if (!/^[1-9][0-9]{0,3}$/.test(raw)) throw new Error('invalid page');
const tags = params.getAll('tag');
if (tags.length > 5 || tags.some(t => !/^[a-z+ ]{1,20}$/.test(t))) {
throw new Error('invalid tags');
}
return {page: Number(raw), tags};
}
assert.deepEqual(readQuery(''), {page:1,tags:[]});
assert.deepEqual(readQuery('?page=2&tag=c%2B%2B&tag=hello+world'),
{page:2,tags:['c++','hello world']});
for (const bad of ['page=1&page=2','page=','page=01','page=0',
'page=10000','admin=true','tag=%ZZ']) {
assert.throws(() => readQuery(bad));
}
console.log(JSON.stringify(readQuery('?page=2&tag=c%2B%2B&tag=hello+world')));
console.log('7 invalid queries rejected');
Run and expected output
node query.mjs
{"page":2,"tags":["c++","hello world"]}
7 invalid queries rejected
Make multiplicity explicit
getAll returns every page value, allowing the parser to reject duplicates before converting anything to a number. A missing page uses the default; a present but empty page fails. The regular expression allows only decimal integers from one through 9999 without leading zeroes, so conversion happens after a narrow syntax check.
Repeated tags remain an array in their original order. URLSearchParams decodes a plus sign as a space in this form-style encoding. A literal plus must therefore be encoded as %2B: c%2B%2B becomes c++, while hello+world becomes hello world.
Edge cases and troubleshooting
The tag grammar is deliberately small and allows only lowercase ASCII letters, plus signs and spaces. It is a demonstration contract, not a universal taxonomy. Define normalization, empty-value handling and duplicate-tag behavior according to the application's requirements before changing it.
URLSearchParams is a decoder, not a strict validator for every malformed percent sequence. The %ZZ fixture is rejected here because its decoded value fails the tag grammar. Do not interpret that result as proof that the constructor rejects malformed encoding generally.
Pass only the search component, such as location.search in a browser, rather than an entire URL. Validate again on the server if this controls server behavior. This program tests parsing in Node; it does not exercise a browser page, framework router, cache or HTTP gateway.
Primary references
The WHATWG URL Standard defines URLSearchParams and form-style query encoding. See the Node assertion reference for the tests. Continue with Javascript tutorials.