Query String Encoding of a JavaScript Object — Build Params Like a Pro
Every frontend developer eventually needs to convert a JavaScript object into URL query parameters.
The naive approach looks like this:
const params = {
q: "javascript tutorial",
page: 1,
limit: 20
};
const url = `https://api.example.com/search?q=${params.q}&page=${params.page}&limit=${params.limit}`;
This works until it doesn't. Add a special character, a nested value, or an array, and the entire query string breaks.
This guide covers how to encode JavaScript objects into query parameters safely, handle nested structures, and avoid the common pitfalls that cause production bugs.
The Wrong Way: Manual String Concatenation
const params = {
q: "javascript & node.js",
category: "programming"
};
const url = `https://api.example.com/search?q=${params.q}&category=${params.category}`;
Output:
https://api.example.com/search?q=javascript & node.js&category=programming
This URL is broken. The & inside the query value is interpreted as a parameter separator. The server receives:
q=javascript
node.js= ← empty value
category=programming
Manual concatenation is fragile, unsafe, and should never be used with user-generated values.
The Right Way: URLSearchParams
Modern JavaScript provides URLSearchParams for safe query string construction.
const params = {
q: "javascript & node.js",
category: "programming"
};
const searchParams = new URLSearchParams(params);
const url = `https://api.example.com/search?${searchParams.toString()}`;
console.log(url);
Output:
https://api.example.com/search?q=javascript+%26+node.js&category=programming
URLSearchParams automatically encodes:
- Spaces as
+ &as%26- Special characters in general
Using the URL Constructor
The URL class combined with URLSearchParams is even cleaner:
const url = new URL("https://api.example.com/search");
url.searchParams.set("q", "javascript & node.js");
url.searchParams.set("category", "programming");
console.log(url.toString());
Output:
https://api.example.com/search?q=javascript+%26+node.js&category=programming
This is the safest approach. The URL object manages everything — base URL, search params, encoding — automatically.
Handling Different Value Types
Strings
url.searchParams.set("q", "hello world");
// Result: q=hello+world
Numbers
url.searchParams.set("page", 1);
url.searchParams.set("limit", 20);
// Result: page=1&limit=20
Booleans
url.searchParams.set("active", true);
// Result: active=true
URLSearchParams converts all values to strings automatically.
Handling Arrays
URLSearchParams does not have native array support. If you pass an array as a value, it calls .toString() on it, which joins with commas:
const params = new URLSearchParams({ tags: ["js", "node", "react"] });
console.log(params.toString());
Output:
tags=js,node,react
This may or may not be what your API expects.
Common Array Serialization Patterns
Repeated Keys (Most Common)
const tags = ["js", "node", "react"];
const params = new URLSearchParams();
tags.forEach(tag => params.append("tag", tag));
console.log(params.toString());
Output:
tag=js&tag=node&tag=react
Many APIs (and frameworks like Express.js, Django, Rails) parse repeated keys into arrays automatically.
Bracket Notation (PHP Style)
const tags = ["js", "node", "react"];
const params = new URLSearchParams();
tags.forEach(tag => params.append("tags[]", tag));
console.log(params.toString());
Output:
tags[]=js&tags[]=node&tags[]=react
PHP-style bracket notation is less common outside PHP but some APIs use it.
Comma-Separated
const tags = ["js", "node", "react"];
url.searchParams.set("tags", tags.join(","));
// Result: tags=js,node,react
Some APIs prefer comma-separated values in a single parameter.
Handling Nested Objects
URLSearchParams does not handle nested objects natively. You need a custom serializer.
Dot Notation Serialization
function serialize(obj, prefix = "") {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
// Recursively serialize nested objects
const nested = serialize(value, fullKey);
nested.forEach((v, k) => params.append(k, v));
} else {
params.append(fullKey, value);
}
}
return params;
}
// Usage
const filters = {
price: { min: 10, max: 100 },
category: "electronics"
};
const params = serialize(filters);
console.log(params.toString());
Output:
price.min=10&price.max=100&category=electronics
Square Bracket Serialization (Rails/Express Style)
function serializeRails(obj, prefix = "") {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}[${key}]` : key;
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
const nested = serializeRails(value, fullKey);
nested.forEach((v, k) => params.append(k, v));
} else {
params.append(fullKey, value);
}
}
return params;
}
// Usage
const filters = {
price: { min: 10, max: 100 }
};
const params = serializeRails(filters);
console.log(params.toString());
Output:
price[min]=10&price[max]=100
Building a Complete URL from an Object
Here is a reusable utility that handles the common patterns:
class QueryBuilder {
constructor(baseUrl) {
this.url = new URL(baseUrl);
}
addParam(key, value) {
if (value === undefined || value === null) return this;
if (Array.isArray(value)) {
value.forEach(v => this.url.searchParams.append(key, v));
} else if (typeof value === "object") {
Object.entries(value).forEach(([k, v]) => {
this.url.searchParams.append(`${key}[${k}]`, v);
});
} else {
this.url.searchParams.set(key, value);
}
return this;
}
addParams(obj) {
Object.entries(obj).forEach(([key, value]) => this.addParam(key, value));
return this;
}
build() {
return this.url.toString();
}
}
// Usage
const url = new QueryBuilder("https://api.example.com/search")
.addParams({
q: "javascript",
tags: ["js", "node"],
page: 1,
filters: { price: 100 }
})
.build();
console.log(url);
Encoding Existing Objects
If you receive an encoded query string and need to modify it:
const existingUrl = "https://example.com/search?q=hello+world&page=1";
const url = new URL(existingUrl);
url.searchParams.set("q", "new search");
url.searchParams.delete("page");
console.log(url.toString());
// https://example.com/search?q=new+search
Dealing with + vs %20
URLSearchParams uses + for spaces. If your API expects %20, you need a different approach.
// URLSearchParams produces +
const params = new URLSearchParams({ q: "hello world" });
console.log(params.toString()); // q=hello+world
// Use encodeURIComponent for %20
const q = encodeURIComponent("hello world");
const url = `https://example.com/search?q=${q}`;
console.log(url); // https://example.com/search?q=hello%20world
Uncommon but Useful Patterns
Removing Undefined or Null Values
const raw = { q: "test", page: null, limit: undefined, sort: "relevance" };
const cleaned = Object.fromEntries(
Object.entries(raw).filter(([_, v]) => v != null)
);
const params = new URLSearchParams(cleaned);
console.log(params.toString()); // q=test&sort=relevance
Encoding Dates
const params = {
from: new Date("2026-01-01").toISOString(),
to: new Date("2026-12-31").toISOString()
};
const url = new URL("https://api.example.com/analytics");
url.searchParams.set("from", params.from);
url.searchParams.set("to", params.to);
console.log(url.toString());
// https://api.example.com/analytics?from=2026-01-01T00%3A00%3A00.000Z&to=2026-12-31T00%3A00%3A00.000Z
Testing Query String Construction
describe("QueryBuilder", () => {
it("encodes simple params", () => {
const url = new URL("https://example.com/search");
url.searchParams.set("q", "hello world");
expect(url.toString()).toContain("q=hello+world");
});
it("encodes special characters", () => {
const url = new URL("https://example.com/search");
url.searchParams.set("q", "javascript & node.js");
expect(url.toString()).toContain("%26");
});
it("handles arrays", () => {
const url = new URL("https://example.com/search");
["a", "b"].forEach(v => url.searchParams.append("tag", v));
expect(url.toString()).toContain("tag=a&tag=b");
});
it("handles empty values", () => {
const url = new URL("https://example.com/search");
url.searchParams.set("q", "");
expect(url.toString()).toContain("q=");
});
it("round-trips correctly", () => {
const original = "javascript & node.js";
const url = new URL("https://example.com/search");
url.searchParams.set("q", original);
const decoded = url.searchParams.get("q");
expect(decoded).toBe(original);
});
});
Best Practices
Always Use URLSearchParams or URL Constructor
// Good
new URLSearchParams({ key: value });
// Bad
`?key=${value}`
Handle Arrays Explicitly
// Know what format your API expects
url.searchParams.append("tags", tag); // repeated keys
url.searchParams.set("tags", tags.join(",")); // comma-separated
Validate Before Building
function buildSafeUrl(base, params) {
const url = new URL(base);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.set(key, value);
}
});
return url.toString();
}
Consider Third-Party Libraries for Complex Cases
For deeply nested objects or complex serialization, consider:
qs— the most popular query string library (supports nesting, arrays, custom formats)query-string— a lighter alternative
import qs from "qs";
const params = qs.stringify({
filter: { price: { min: 10, max: 100 } },
tags: ["js", "node"]
});
console.log(params);
// filter[price][min]=10&filter[price][max]=100&tags[0]=js&tags[1]=node
Related Resources
For more on encoding techniques and cross-language patterns:
-
How to Encode a URL in JavaScript JavaScript URL Encoding Guide
-
URL Encoding the Space Character Space + vs %20
-
How to URL Encode a Query String in Python Python URL Encoding
-
Common URL Encoding Mistakes Developers Keep Making Common Mistakes
FAQ
What is the safest way to build a query string in JavaScript?
Use the URL constructor with searchParams. It handles encoding automatically and reduces human error.
Does URLSearchParams handle nested objects?
No. You need a custom serializer or a library like qs for nested object serialization.
How do I handle arrays in query parameters?
Use append() for repeated keys (?tag=a&tag=b) or set() with a comma-separated value.
Why does URLSearchParams use + for spaces?
Because URLSearchParams follows the application/x-www-form-urlencoded standard, which uses + for spaces.
How do I use %20 instead of + with URLSearchParams?
You cannot change URLSearchParams behavior. Use encodeURIComponent() manually if you need %20.
What is the difference between URLSearchParams and qs?
URLSearchParams is a native browser API with basic features. qs is a third-party library that supports nesting, arrays with indices, and custom formats.
How do I remove null or undefined values before building a query string?
Filter the object entries before passing them to URLSearchParams:
const cleaned = Object.fromEntries(
Object.entries(raw).filter(([_, v]) => v != null)
);
Final Thoughts
Building query strings from JavaScript objects is a task that seems trivial but hides real complexity. Special characters, arrays, nested objects, and encoding conventions all create opportunities for bugs.
The URL constructor with searchParams covers 90% of use cases safely. For complex serialization needs, build a custom serializer or use a dedicated library. But never manually concatenate query strings — that approach breaks the moment real user data enters your application.
For quick verification of how your object gets encoded, the URL Encoder/Decoder tool provides immediate feedback on different encoding approaches.