Backend Systems · API Styles

Resource Identity and URI Design in REST

How REST separates the name of a thing from the bytes that represent it, and which URI choices survive changes to the underlying data model.

A REST interface is built from one unusual commitment: every piece of state worth talking about gets a name, and that name is a URI. The name is not a function. It does not describe an operation, and calling it does not invoke a procedure. It identifies a resource, which is an abstraction over some set of values that can change over time, and the HTTP method chosen for a request says what to do with the current representation of that resource. Confusing the name with the operation is the source of most of the design taste failures in interfaces that claim to be RESTful.

The commitment has consequences that are easy to state and hard to follow. A URI must mean the same thing to every client that sees it, must remain stable across server restarts and deployments, and must not encode the current implementation. It must also be unambiguous: two spellings of the same resource are two resources as far as caches, logs, and clients are concerned.

A resource is a name, not a file

A resource is a mapping that may change at any time. /articles/42 identifies an article; the article’s title, body, and publication date may change, and the resource still has the same name. What a request retrieves is a representation: bytes in some media type that describe the resource’s current state at the time the response was generated. The representation is not the resource, and the server is free to produce different representations for the same URI depending on the Accept header, as content negotiation covers in detail.

This separation is what allows caching to work at all. A cache key is the URI plus the method plus the selected representation’s metadata. If the URI changed whenever the underlying state changed, the cache would never hit. It also explains why URIs should not contain timestamps of the state they return, random nonces, or per-request sequence numbers: those make every request name a distinct resource that will never be requested again.

Fielding’s dissertation describes resources as conceptual mappings rather than stored entities, and that framing resolves an old argument. A URI does not have to correspond to a row, a file, or an object. It may correspond to the result of a query, an aggregate computed on demand, or a view over several tables. What it must do is behave consistently: the same name, at the same moment, must produce a representation consistent with the same resource state for every client.

Hierarchy encodes lookup, not ownership

Path segments imply a tree, and the tree is a naming device. /customers/17/orders/93 says that order 93 is reachable through customer 17, which usually means the order belongs to that customer, but the URI grammar has no opinion about foreign keys. The relationship is a convention enforced by the server, and the server may accept several paths that reach the same resource.

Collection and item resources form the basic pair. /orders is the collection; /orders/93 is a member. Collections should be filterable with query parameters rather than by adding path depth: /orders?status=open&limit=50 names the same resource as /orders with a narrower representation, and the query string is part of the URI for cache-key purposes, which is precisely the desired behaviour. Depth beyond three or four segments tends to signal that a join is being encoded in the path; a top-level resource with a query parameter is usually easier to keep stable.

Two naming patterns commonly cause trouble. The first is the verb-in-path endpoint, such as /orders/93/cancel. It is a procedure wearing a resource’s clothes, and it breaks the uniform interface because a cache cannot reason about it and a retry may cancel twice. Where the state transition is genuinely not expressible as a replacement of a representation, the pragmatic forms are a sub-resource that accepts a POST, such as /orders/93/cancellation, or a PATCH against the order that sets a status field with a conditional request. The second is the deep singleton, such as /customers/17/settings/theme. It works, but if only one child is ever addressable the extra level adds no naming power.

Choosing between path and query parameters

The rule of thumb is that path segments identify, query parameters select and shape. A path segment changes which resource is addressed; a query parameter narrows or formats what is returned from that resource. Moving a parameter between the two changes the identity of the URI and therefore the cache key, the logs, and any client that hard-coded the old form.

Query strings have their own grammar problems. RFC 3986 defines the query component as an opaque sequence of characters and leaves the key=value pairs separated by & to the application, so the generic syntax says nothing about ordering or repeated keys. The application/x-www-form-urlencoded format used by HTML forms defines percent-encoding of spaces as +, while generic URI syntax requires %20; a server that accepts both must normalise. Repeated keys are not ordered, so ?tag=a&tag=b and ?tag=b&tag=a should select the same set, and an API that assigns meaning to parameter order is relying on a convention no intermediary understands. Arrays are better expressed with repeated keys or with a comma-separated value whose encoding is documented, not with ad hoc bracket syntax that looks like a PHP artefact.

Normalisation, case, and the canonical form

RFC 3986 defines the unreserved set as the 26 uppercase letters, the 26 lowercase letters, the ten digits, and the four characters -, ., _, and ~, for 66 characters that never need percent-encoding. Everything else in a path segment must be percent-encoded, and the encoding of a UTF-8 byte sequence is the standard way to carry non-ASCII text. Uppercase and lowercase hexadecimal digits in a percent-escape are equivalent.

Scheme and host are case-insensitive; the path is not, and neither is the query. A server that treats /Orders and /orders as the same resource is making an extra-grammatical promise, and it must then issue a redirect or otherwise converge on one spelling, or caches will store two entries. The same applies to a trailing slash, to a default port written explicitly, and to dot segments: RFC 3986 defines syntax-based normalisation that removes . and .. segments, lowercases the scheme and host, and decodes percent-encoded unreserved characters. A sensible server normalises incoming request targets to one canonical form, answers 301 for the alternates, and never generates a link in a non-canonical form.

Percent-encoded slashes deserve separate care. %2F inside a path segment is not a segment separator according to the grammar, but many servers decode before routing and therefore cannot distinguish /a%2Fb from /a/b. Treating encoded separators as data requires routing on the raw target, which some frameworks and reverse proxies do not support. Where identifiers may contain slashes, an encoding that avoids the character entirely, such as base64url or a UUID, is safer than relying on %2F.

Identifiers that outlive the schema

The identifier embedded in the URI should be stable for the lifetime of the resource. Sequential integers are compact and index well, but they leak volume and invite enumeration. UUIDs avoid both problems at the cost of 16 bytes and poorer locality in a B-tree index; a v4 UUID is 122 bits of randomness and cannot be ordered, while time-ordered variants trade some unpredictability for index locality. Neither choice affects the URI grammar, but both affect what a client can infer from a name, and enumeration is often the reason to prefer the wider identifier.

Natural keys, such as an email address or a stock symbol, are tempting because they are readable. They are risky because they can change, and a URI that changes is a broken link unless the server keeps a permanent redirect table from the old form. Exposing a surrogate key and letting the natural key live in the representation decouples the name from mutable business data. When a resource must move, a 301 with Location preserves the old name’s usefulness, and a 308 preserves the method as well, which matters for anything but GET.

Where strict resource modelling breaks down

Some operations are not resource replacements: search with a large structured query, bulk import, and report generation are the usual examples. POST to a query resource, which returns a representation of the result set or a reference to it, keeps the interface uniform at the cost of an extra round trip or a stored job. A GET with a long query string is cacheable and simple but can exceed intermediary URL limits, and no specification fixes a maximum length, so behaviour differs between proxies. The honest engineering choice is to document the limit and avoid designs that depend on multi-kilobyte paths.

Hypermedia links are the mechanism that lets a server change its URI space without breaking clients, and they are also the part of REST most often skipped. A client that constructs /orders/93/items from a template is coupled to the layout; a client that follows a link from the order representation is not. The cost is a larger payload and a client that must understand link semantics. Between the two extremes, publishing the URI templates and treating them as contractual is a defensible middle, and schema evolution is where that contract gets tested.

References

  1. RFC 3986: Uniform Resource Identifier (URI): Generic Syntax — IETF
  2. RFC 9110: HTTP Semantics — IETF
  3. Architectural Styles and the Design of Network-based Software Architectures — Roy Fielding, doctoral dissertation, 2000