Complex data structures don't map naturally to URL query strings. Different conventions exist for encoding nested objects, each with trade-offs.
Key Takeaways
- 1Bracket notation: filter[status]=active&filter[type]=premium
- 2Dot notation: filter.status=active&filter.type=premium
- 3JSON encoding: data={"status":"active"} (needs URL encoding)
- 4Consider URL length limits with deeply nested data
- 5Use POST with JSON body for complex data when possible
“qs allows you to create nested objects within your query strings, by surrounding the name of sub-keys with square brackets []. For example, the string 'foo[bar]=baz' converts to{ foo: { bar: 'baz' } }.”— qs library documentation
Bracket Notation
Bracket notation is the most widely adopted convention for encoding nested objects in URLs. If you've worked with PHP, Ruby on Rails, or the popular qs JavaScript library, you've already seen this pattern. Each level of nesting adds another set of square brackets to the parameter name.
https://api.example.com/users?filter[status]=active&filter[role]=admin&sort[field]=name&sort[order]=asc// Using qs library for nested objects
import qs from 'qs';
const params = {
filter: {
status: 'active',
role: 'admin',
created: {
gte: '2024-01-01',
lte: '2024-12-31'
}
},
sort: {
field: 'name',
order: 'asc'
}
};
qs.stringify(params);
// "filter[status]=active&filter[role]=admin&filter[created][gte]=2024-01-01&filter[created][lte]=2024-12-31&sort[field]=name&sort[order]=asc"
// Parsing back
qs.parse('filter[status]=active&filter[role]=admin');
// { filter: { status: 'active', role: 'admin' } }This code demonstrates the full round-trip: you start with a nested JavaScript object, stringify it into URL parameters with bracket notation, and parse it back into the original structure. The qs library handles all the encoding and decoding automatically, preserving your data structure through the URL.
While bracket notation works well for most cases, it can become unwieldy with deeply nested structures. Let's look at an alternative approach that produces more compact URLs.
Dot Notation
Dot notation uses periods to separate nesting levels, similar to how you access properties in JavaScript. It produces shorter URLs than bracket notation and feels natural to developers. However, it's less commonly supported out of the box—you'll likely need to write custom parsing logic.
https://api.example.com/search?filter.category=electronics&filter.price.min=100&filter.price.max=500// Flatten object to dot notation
function toDotNotation(obj, prefix = '') {
const result = {};
for (const [key, value] of Object.entries(obj)) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
Object.assign(result, toDotNotation(value, newKey));
} else {
result[newKey] = value;
}
}
return result;
}
const params = {
filter: {
category: 'electronics',
price: { min: 100, max: 500 }
}
};
const flat = toDotNotation(params);
// { 'filter.category': 'electronics', 'filter.price.min': 100, 'filter.price.max': 500 }The toDotNotation function recursively walks through your object and creates flat keys joined by dots. Notice that arrays require special handling—this basic implementation treats them as objects, which may not be what you want. Consider extending it to handle arrays explicitly for your use case.
Both bracket and dot notation require specific parsing on the server. For maximum flexibility with any structure, you might consider encoding the entire object as JSON.
JSON Encoding
When your data structure is too complex for bracket or dot notation, you can encode the entire object as JSON and pass it as a single parameter. This approach handles any structure—nested objects, arrays, mixed types—but comes with significant trade-offs you should understand.
// Encode entire object as JSON
const params = {
filter: { status: 'active', tags: ['featured', 'sale'] },
sort: { field: 'price', order: 'desc' }
};
const url = new URL('https://api.example.com/products');
url.searchParams.set('query', JSON.stringify(params));
// Result (before URL encoding):
// ?query={"filter":{"status":"active","tags":["featured","sale"]},"sort":{"field":"price","order":"desc"}}
// URL encoded version is much longer...
// Consider URL length limits!While JSON encoding preserves your entire data structure, the URL-encoded result is significantly longer than the original JSON. Characters like {, ", and : all require percent-encoding, which can easily push your URL over browser or server limits. Use this approach sparingly and always check the resulting URL length.
Now that you've seen all three approaches, let's compare them side by side to help you choose the right one for your API.
Format Comparison
The table below summarizes the trade-offs between each encoding format. Your choice depends on your backend framework, how deeply nested your data is, and whether URL length is a concern.
| Format | Pros | Cons |
|---|---|---|
| Brackets | Widely supported, readable | Verbose with deep nesting |
| Dots | Compact, simple parsing | Less common, ambiguous with dots in keys |
| JSON | Handles any structure | Long URLs, needs encoding, harder to read |
| Flat keys | Simple, works everywhere | Loses structure information |
Regardless of which format you choose, there are some universal guidelines that will help you avoid common pitfalls when passing complex data through URLs.
Best Practices
The most important principle is knowing when not to use URL parameters for complex data. URLs have practical length limits, and deeply nested structures quickly become unwieldy. Here are concrete guidelines that will save you headaches down the road.
// 1. Keep nesting shallow (2-3 levels max)
// Good
{ filter: { status: 'active' } }
// Avoid
{ filter: { user: { profile: { settings: { theme: 'dark' } } } } }
// 2. Use POST for complex data
// Instead of encoding this in URL:
const complexFilter = { /* lots of nested data */ };
// POST it in the request body:
fetch('/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(complexFilter)
});
// 3. Consider URL length limits
// See /docs/parameters/max-length for detailsThe key takeaway here is pragmatism: use URL parameters for simple filters and state that you want to be bookmarkable or shareable, but switch to POST requests with JSON bodies when your data exceeds two or three levels of nesting. This gives you the best of both worlds—clean URLs for simple cases and unlimited flexibility for complex queries.