Once a client receives a resource's URL, it may keep using that address long after the original request. A checkout client, for example, might save a reservation URL to inspect the hold later. If that URL contains a warehouse name, the reservation's current status, or a backend script filename, an internal change can break the reference even though the reservation still exists.
Good URL design gives resources clear, dependable addresses.
This chapter covers naming, path and query choices, encoding, and consistent URL handling. The examples use a fictional bookstore API over HTTPS. Naming choices are conventions for this API; URI syntax and HTTP behavior impose separate requirements.
A URL is an address for locating a resource. Consider https://api.bookstore.example/books?format=paperback.
The scheme, https, specifies secure HTTP access. The authority identifies the server, here the host api.bookstore.example. The path, /books, contains slash-separated segments. The query, format=paperback, follows ?. For HTTP, the path and query together identify the target within the server's namespace.
The diagram separates the server address from the target this API interprets:
The service gives format its meaning. Here it selects the physical book format, not the response encoding. The query still helps identify the requested view, even when the path names a collection.
A URL can also include a fragment, which begins with #. The client does not send a fragment as part of the HTTP request target. Therefore, /books#book_1042 cannot tell this API to retrieve book book_1042. Use a path or query contract the server actually receives.
Choose names from the vocabulary consumers use to describe their work. For this bookstore, books, orders, and reservations are clearer than catalog-records, sales-entities, and inventory-lock-manager.
Use plural nouns for collection paths and place an item's identifier after its collection. This gives /books and /books/book_1042 a predictable relationship. A singleton, a resource with exactly one instance in its context, can use a singular name such as /account/profile.
The following examples are intentionally flawed for the bookstore's chosen style. Each exposes an implementation detail, an unnecessary operation name, or an inconsistent collection name:
Use lowercase words and hyphens in fixed path segments. Underscores are valid URL characters, so an existing API that consistently uses them does not need a disruptive rename merely to adopt hyphens. Consistency across the API matters more than this particular stylistic choice.
Separate names you control from identifiers callers supply as data. The hyphen convention for /reading-lists does not require changing list_731. Likewise, do not lowercase an opaque identifier, one whose contents clients should not interpret or modify.
HTTP scheme and host names are case-insensitive; servers generally compare paths and queries case-sensitively. Publish /books, and do not assume /Books reaches the same route. A service may deliberately accept aliases, but clients should use the documented spelling.
For this API's ordinary JSON resources, omit .json and use Accept: application/json. A genuine downloadable artifact can still have a meaningful filename such as catalog.csv. The aim is a useful address, not a blanket ban on file extensions.
Use the path to identify the resource or collection. Use query parameters to select items or control the collection's response, following the API's documented rules—for example, to select paperback books. This is a practical convention, not a protocol rule that identifiers must always appear in paths.
For this API, /books/book_1042 addresses one book. /books?format=paperback addresses a selected catalog view. A title search belongs in a query such as /books?title=Designing%20Reliable%20Services; putting the title in /books/Designing-Reliable-Services would also require decisions about duplicate titles and title changes.
Avoid turning independent selection criteria into fixed path levels. An intentionally awkward design such as /books/format/paperback/language/en makes callers learn a path ordering for combinations of criteria. This API uses /books?format=paperback&language=en instead.
Assume format accepts paperback, hardcover, or ebook, and language accepts supported language codes. Both parameters are optional and may appear at most once. The public catalog request is:
For an example catalog with one matching book, the response is:
Each JSON response body occupies one line without a trailing newline. The response envelope and cache policy are example choices. With no matching books, this contract returns the same successful envelope with "items":[].
The query needs a parsing contract as well as parameter names. This API rejects unknown parameters, empty values for these two parameters, and repeated occurrences. Rejecting unknown parameters helps catch a typo such as langauge=en instead of silently returning a broader result. An established API may choose to ignore unknown parameters for compatibility, but it should make that behavior deliberate.
Consider a request with conflicting occurrences:
The API rejects it under its single-value rule:
Some APIs intentionally use repeated parameters for lists. That is a different contract. Problems arise when one component takes the first value while another takes the last. The gateway and application must agree on the interpretation.
For the distinct parameters above, this API treats parameter order as irrelevant. That does not mean every URL processor or cache automatically merges differently ordered query strings. Generate links in a consistent order, and do not reorder repeated parameters or signed URLs without knowing their contract.
Include a parent in a path when it supplies meaningful scope. /orders/ord_204/lines/line_2 can be useful when a line's identifier is meaningful only within its order. The path tells the server which order owns that line.
Avoid carrying every known relationship into the address. If reservation IDs are unique across this service, /reservations/res_731 is sufficient. A path such as /warehouses/wh_8/customers/cus_42/books/book_1042/reservations/res_731 makes clients supply facts they should not need for a reservation lookup. It also risks tying the public address to allocation details.
There is no universal maximum nesting depth. A useful test is whether each parent is necessary to identify or scope the target. Relationships alone do not justify extra segments.
Keep mutable properties out of the canonical item address. In this API, a reservation keeps its URL when it changes from active to expired:
The diagram shows two observations of the same resource, not two resources requiring different addresses. A view such as /reservations?status=active can change its membership without changing the addresses of individual reservations.
Apply the same reasoning to deployment details. A public route should not need to change because the service moves to another programming language or storage shard. Keep names such as node-service, shard-7, and new-backend out of consumer-facing paths.
If an API already has a base prefix such as /v1, apply the established prefix consistently. This chapter's bookstore examples have no version prefix. Adding one is an API evolution decision, not a prerequisite for a valid resource URL.
URL construction must preserve the distinction between structure and data. Concatenating a customer's text directly after ?title= can accidentally create additional parameters.
Suppose the intended title is C++ & APIs. The correctly encoded query in this example is /books?title=C%2B%2B%20%26%20APIs. The plus signs and ampersand belong to the title. They are not instructions to split or reinterpret the query.
Percent-encoding represents a byte as % and then two hexadecimal digits. Encode each component's data with a URL library, not the fully assembled URL, and avoid double encoding or repeated decoding.
Common encodings for query values include:
Query parsers using form-encoding conventions interpret + as a space. That behavior is not a universal rule for every URL component. Encoding a literal plus as %2B avoids losing it under that convention. A URL builder may serialize spaces as + when using form encoding; the server must use compatible parsing.
For non-ASCII text, agree on a character encoding. This API uses UTF-8, so the query value café can appear as caf%C3%A9. Let the library encode the text rather than manually encoding individual characters.
For path identifiers, this bookstore deliberately uses a restricted alphabet of letters, digits, underscores, and hyphens. An identifier containing / needs special care: %2F is not generally interchangeable with a path separator, and every routing component must agree on its treatment. Choosing IDs that do not need encoded separators avoids that ambiguity.
Keep external names distinct from route structure. If an imported book code contains punctuation that the path contract excludes, offer a documented lookup parameter instead of asking callers to invent escaping rules. Check incoming requests consistently for malformed encodings and unsupported identifier forms.
A canonical URL is the address the API chooses to publish consistently for a target. Pick a policy for trailing slashes, route spelling, and aliases before consumers build integrations around them.
This API publishes collection and item paths without trailing slashes. /books and /books/ are not automatically equivalent addresses. Here, the service deliberately redirects the latter to the former.
The client requests the alternate path:
The server responds:
The redirect points to the published address. A 308 preserves the request method when the client follows it. Do not assume a client follows redirects automatically; document the canonical address and generate it correctly in the first place.
This flow illustrates the extra exchange for a client that follows the redirect:
If this API accepts /books/?format=paperback, its redirect must retain the selection in /books?format=paperback. Dropping the query would change the requested view. An alternative service could reject noncanonical paths instead; what matters is a documented policy the service applies consistently.
Avoid aggressive normalization. Lowercasing the entire URL can alter an ID or a case-sensitive value. Decoding everything before routing can change the apparent segment boundaries. Transform a URL component only when its rules define what that transformation means.
Reserve fixed routes before accepting unrestricted item names. For example, a literal /books/search route can collide with /books/{book_id} if search is a valid ID. The bookstore examples in this lesson use query-based search. The documented book_ identifier prefix also leaves room for a fixed route such as /books/search, which the later search lesson introduces. Braces in route templates are placeholders, not literal characters clients send.
URLs can appear in logs, browser history, and copied diagnostics. HTTPS does not prevent those local disclosures. Keep passwords, bearer tokens, and unnecessary personal details out of paths and queries.
An intentionally unsafe design is /reservations/res_731?access_token=EXAMPLE_TOKEN. This API supplies the token through the Authorization header instead. Headers also need appropriate logging controls, but credentials no longer become part of the resource's address.
A predictable URL is not an authorization decision. A customer who changes /reservations/res_731 to another ID must still pass the service's access checks. Likewise, a parent segment such as /customers/cus_42/reservations does not prove the caller owns that customer account. Validate access to the resolved resource, using the same interpretation of the path that routing uses.
URL design can reduce accidental disclosure and parsing ambiguity. It cannot replace permission checks or turn an identifier into proof of ownership.
Use clear resource names, consistent collection paths, and addresses that survive changes to resource state and implementation. Give query parameters an explicit contract, including duplicate and empty-value behavior.
Encode values with component-aware libraries, publish canonical URLs, and keep sensitive data out of addresses. Naming conventions help clients predict the interface; protocol rules and consistent parsing make those addresses dependable.