Designing REST API URLs: Paths, Params, and Versions
REST API URL design determines how clients discover and use your endpoints. Well-structured URLs are predictable, self-documenting, and tolerant of client-side parsing. Poorly structured URLs force clients to pattern-match against strings, build fragile regexes, and handle edge cases they should never have to consider. The core decisions about when to use path segments versus query parameters, how to version the API, and how to handle filtering and pagination shape the API's usability for years.1
URL design is not standardized by REST itself, but consistent conventions have emerged across widely used APIs. Path segments identify resources; query parameters filter, sort, and paginate; the HTTP method expresses the operation. Versioning belongs in the path or the Accept header, never in a query parameter that clients might cache differently.
Path segments versus query parameters
Path segments identify a specific resource: /users/123 identifies user 123, and /orders/456/items identifies the items collection within order 456. Query parameters filter, sort, or modify the representation: /users?role=admin&page=2. Consequently, path parameters are required because a request without the user ID makes no sense. Query parameters are optional because a request without pagination defaults to the first page. Building on this, avoid putting filter conditions in path segments: /users/active is awkward to extend, while /users?status=active is composable. Resources with multiple identifiers, like a resource owned by both a user and an organization, may appear in nested paths or at the top level depending on your access control model.
Versioning strategies
Path versioning (/v1/users, /v2/users) is the most widely supported approach because every HTTP client, proxy, and caching layer understands URL paths.1 Building on this, the version prefix should be the first path segment after the domain, making it obvious in logs and documentation. Teams that skip version discovery entirely often regret it once they need to introduce a breaking change without alienating existing consumers.
When to bump the major version
Semantic versioning beyond major versions adds noise since /v1.2/users is unnecessary in practice. Reserve a version bump for breaking changes only: new optional fields, new endpoints, and new query parameters are backwards-compatible and do not need a new version. Accept header versioning (Accept: application/vnd.api+json;version=2) is cleaner in theory but requires middleware that most clients do not send by default, so path versioning remains the safer choice for public APIs.
Encoding and special characters
Path segments may contain hyphens, underscores, tildes, and alphanumeric characters without encoding.2 Spaces must be encoded as %20, not +. Consequently, avoid slashes in resource identifiers because they create ambiguity in path segment boundaries. For IDs that contain slashes (like base64-encoded values), encode the / as %2F or use a URL-safe encoding like base64url. Query parameter values should be encoded with encodeURIComponent() on the client, since this handles spaces, ampersands, and equals signs that would otherwise break the query string. Building on this, document which parameters accept comma-separated values (?ids=1,2,3) and which accept repeated keys (?id=1&id=2) because both patterns are common in practice but parsing them interchangeably leads to subtle bugs.
Search, filter, and sort conventions for API query parameters
Consistent query parameter conventions make your API predictable for clients. Use q for full-text search (?q=url+parsing), sort for field ordering (?sort=-created_at for descending), and limit/offset or page/per_page for pagination. For complex filtering, two conventions exist: operator suffixes (?price_gte=10&price_lte=100) or bracket notation (?filter[price][gte]=10). The operator suffix approach is more readable in raw URLs; bracket notation maps more naturally to nested objects in server-side parsing. Pick one convention and document it in your API reference; mixing both confuses client developers and leads to inconsistent usage.
Comma-separated values versus repeated keys for array parameters
When a query parameter accepts multiple values, you must choose between comma-separated (?ids=1,2,3) and repeated keys (?ids=1&ids=2&ids=3). The comma-separated form is more compact and easier to read, but splitting a comma-joined ids list back into an array means writing your own split-and-trim logic on the server. Repeated keys are handled natively by most URL parsing libraries (URLSearchParams.getAll('ids'),3 parse_qs in Python4) but produce longer URLs. For APIs consumed by JavaScript clients, repeated keys are more natural because URLSearchParams handles them automatically. For APIs consumed by curl or command-line tools, comma-separated values are easier to type.
Whichever you pick, document it next to the endpoint definition so clients do not guess and mix the two styles on the same parameter. A schema with an explicit array type communicates the expectation more clearly than prose, and code generation tools will produce the correct client types from it. If you must support both for backward compatibility, normalize to repeated keys internally and treat the comma form as a convenience that you expand before validation.
HATEOAS and discoverable API URL structures
HATEOAS (Hypermedia as the Engine of Application State) is a REST constraint where API responses include links to related resources rather than requiring clients to construct URLs from documentation.5 A response from /orders/123 includes a links object with self: /orders/123, customer: /customers/456, and items: /orders/123/items. The client follows these links without needing to know the URL structure. Building on this, HATEOAS makes URL changes transparent to clients: if you migrate from /orders/123 to /v2/orders/123, only the server-side link generation changes; clients that follow links continue to work.
Content negotiation and URL-based format selection
APIs that support multiple response formats (JSON, CSV, XML) can use the Accept header for content negotiation or a URL-based format selector (.json, .csv, or ?format=json). URL-based format selection is simpler for clients (no header configuration) and works with curl and browser address bars. Implement it with a route pattern like /api/users.:format or a query parameter like /api/users?format=csv. The Accept header approach is more correct per HTTP semantics but harder to use from simple clients. Many APIs support both: check the format parameter first, fall back to the Accept header, and default to JSON if neither is specified. CapyToolkit offers a URL Parser tool that breaks down any URL into its components for inspection and debugging.
When to use this
Apply these patterns when designing new REST API endpoints, reviewing an existing API for consistency, or documenting URL conventions for a team so that new endpoints follow the same structure.
Examples
Resource URL patterns — good versus avoid
// Avoid — verb in path, filter in path, redundant nesting GET /getUsers GET /users/active GET /organizations/1/users/2/orders/3/items
// Prefer — noun paths, filters as query params, flat where possible GET /users GET /users?status=active GET /orders/3/items
Building a versioned API URL with query parameters in JavaScript
const base = "https://api.example.com"; const url = new URL(`${base}/v1/users`); url.searchParams.set("role", "admin"); url.searchParams.set("page", "2"); url.searchParams.set("per_page", "50"); console.log(url.href); // https://api.example.com/v1/users?role=admin&page=2&per_page=50
- 1.
Microsoft, "Web API Design Best Practices," learn.microsoft.com, accessed June 2026. https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design
- 2.
T. Berners-Lee, R. Fielding, and L. Masinter, "Uniform Resource Identifier (URI): Generic Syntax," RFC 3986, IETF, July 2005. https://rfc-editor.org/rfc/rfc3986.html
- 3.
"HATEOAS," Wikipedia, accessed June 2026. https://en.wikipedia.org/wiki/HATEOAS
- 4.
Mozilla Developer Network, "URLSearchParams: getAll() method," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/getAll
- 5.
Python Software Foundation, "urllib.parse — Parse URLs into components," docs.python.org, accessed June 2026. https://docs.python.org/3/library/urllib.parse.html