First of three. This part separates the three contracts that API designs keep trying to make one mechanism carry. Part II asks when runtime controls are worth their cost. Part III builds the hybrid.
One line of markup, barely changed since 1993, underwrites the most successful distributed system anybody has shipped.
Cancel order
The browser understands a great deal about that line. It knows that this is an anchor, that the value in quotes is a URI, that the URI can be resolved against the current document, that activating it retrieves another representation, and that the response will carry a status and a media type. None of that knowledge is specific to this order or this server.
About cancelling, it understands nothing. It does not know whether cancellation is free, whether it can be undone, how it differs from a refund, or whether it is the right response to a customer asking to change an address.
Somebody supplies that judgment. On the Web it is normally a person who read "Cancel order" and already understood orders. The browser supplies uniform mechanics. The reader supplies a goal and domain knowledge. Together they make a client that can enter an application it has never seen and still get useful work done.
That observation is often turned into a verdict: HATEOAS works for humans and is wasted on machines. The verdict is too broad. It correctly identifies a job hypermedia does not do, then treats that job as the only one worth doing.
The better question is which contract a control actually carries.
#What the constraint promises
Fielding's fourth uniform-interface constraint concentrates control state in the representations a client receives. The server presents possible transitions; the client chooses one. In a JSON format, the familiar minimal example looks like this:
Ship the order and the cancel control disappears. The useful claim is not that
the client learns the meaning of cancellation from six letters. It is that the
server remains authoritative about whether this particular order currently
offers that transition and where the transition leads.
That is narrower than the claim usually made for HATEOAS, but it is not small. Every team has shipped some version of the bug where a client inferred "cancellable" from a stale status mapping and the server rejected the call. A control derived from the state and policy the server actually holds can remove that duplicated inference.
It cannot remove the final race. The order may ship after the representation is read and before the action is submitted. The server must still authorize and validate every transition. Hypermedia makes the client's information more current; it does not make distributed state stop changing.
Fielding is also less magical about prior knowledge than some summaries of REST. Clients know protocols, media types, relation types and vocabularies. Some of that vocabulary may be domain-specific. The constraint does not eliminate the need for a shared language; it tries to concentrate that language in reusable processing models and put the service-specific choices in-band.
The argument therefore is not coupling versus no coupling. It is what kind of coupling exists, where it is recorded, and how often it changes.
#Three contracts hiding in one control
cancel appears to be one fact. It is at least three.
| Contract | Question it answers | Typical change rate |
|---|---|---|
| Operation semantics | What does cancellation mean, what inputs does it take, and what are its consequences? | slow |
| Current applicability | May this caller cancel this order in its present state? | fast |
| Invocation binding | Where and how is that transition invoked, and which arguments are already known? | medium or fast |
The first is a dictionary entry. The second is a statement about a situation. The third connects the two to a transport.
A relation and an href compress all three into a small control, but they do not
describe all three equally well. cancel names a concept only a domain-aware
consumer can interpret. Its presence is good evidence of current applicability.
The href binds that applicability to an invocation target. Basic HAL says
nothing about a submission method or request fields; form-oriented hypermedia
formats do, and Part III takes them seriously.
OpenAPI and RPC systems usually arrange the contracts differently. They define the operation and its invocation in a description that a client can compile, then leave current applicability to status fields, prose, policy checks or execution errors. Neither arrangement makes a contract disappear. Each makes a different one authoritative at a different time.
This distinction is the spine of the series:
Operation semantics, current applicability and invocation binding are different contracts with different update frequencies.
Once they are separated, the old choice between a typed API and HATEOAS stops looking binary.
#The generated client drew one contract and omitted another
The ordinary machine integration makes the split visible. You are handed an
OpenAPI document, run a generator, and receive a client with one method per
operationId, typed parameters and typed responses. Then you write:
const order = await api.getOrder({ orderId });
if (order.status === "pending") {
await api.cancelOrder({ orderId, reason });
}
The generated client knows that cancelOrder exists, where to send it and what
arguments it accepts. It does not know why the pending branch is correct. The
generator gave cancelOrder, refundOrder and createReturn the same standing;
the developer supplied the lifecycle from documentation and memory.
A bare cancel link would not teach the client the lifecycle either. What it can
do is replace the hand-drawn applicability row with a current statement: this
order offers cancellation now. Richer controls can also bind the order ID and
submission target. That is already enough to matter, even though the meaning of
cancellation remains shared knowledge.
The missing row therefore does not show that clients should know nothing. It shows that they carry two very different kinds of knowledge, and only one of them is a plausible target for architectural decoupling.
#Semantic coupling is not a design failure
An invoicing client coupled to the meaning of invoices is not broken. That
coupling is why the client exists. A refund processor has to know what a refund
does regardless of whether it calls refundPayment(id), follows a
refund-payment relation, or submits an HTML form labelled "Refund."
What architecture can remove is accidental knowledge: which internal service owns a route, how identifiers are interpolated, whether a target is regional or signed, which transition is currently legal, or which fields the server can already bind. Those are implementation or situation facts rather than the business purpose of the client.
This gives hypermedia a more defensible ambition. It does not decouple the consumer from the domain. It can decouple the consumer from parts of the domain's current arrangement.
The value depends on how expensive those parts are. Interpolating an order ID into a stable path is cheap accidental knowledge. Reproducing an authorization policy that depends on fulfilment state, warehouse ownership and account risk is not. “Hypermedia hides URL construction” is therefore both true and too small: a useful control hides or reports whatever its media type is capable of carrying. A link-only format carries little. A form can carry considerably more.
The distinction also explains why a new relation is not automatically useful. An adaptable client may be able to read a description of a relation it has never seen, and a language model may reason about that description. But a new URI alone does not teach a new business concept. Whether descriptions are sufficient for genuinely novel operations is now an empirical question, not a property delivered by the link syntax.
That leaves a practical question: if the link does not supply meaning, where does the shared meaning come from? The answer is the same place it comes from in every interface — a vocabulary — but hypermedia makes the shape of that vocabulary especially visible.
#Shared vocabulary is still a contract
The IANA link-relation registry contains a compact vocabulary that transfers
well between domains: next, prev, first, last, up, alternate,
describedby, edit, search. A generic client can attach useful behavior to
those relations because their semantics were standardized elsewhere and stay
roughly the same everywhere.
Business verbs do not transfer as easily. cancel-order, approve-loan and
rotate-credentials carry rules specific to their domains. RFC 8288 permits
extension relation types identified by URIs, and Fielding explicitly allows
domain-specific vocabulary. You can publish
https://example.com/rels/cancel-order, give it a definition, and let clients
learn it.
The URI solves identity, not comprehension. A client still needs the relation's
definition, a domain model or a description it can reason about. That is not a
failure of RFC 8288; every protocol eventually rests on shared meaning. It does
mean that replacing cancelOrder with a relation URI has not removed the
semantic contract. It has changed how that contract is named and distributed.
The comparison becomes more useful when stated without absolutes:
| Interface style | Stable meaning lives in | Runtime representation can add |
|---|---|---|
| Generated RPC or OpenAPI client | operation description and client code | result data and errors |
| Basic link-style hypermedia | media type, relation definition and prose | relation presence and target |
| Form-oriented hypermedia | media type, relation definition and form rules | presence, target, method and current inputs |
None is contract-free. The important difference is which parts tooling can check before deployment and which parts remain authoritative only at runtime.
At one end of that spectrum, the shared vocabulary is so small and settled that the runtime control feels effortless. That is why the most successful machine hypermedia example is also the least dramatic one.
#The relation everybody follows
Pagination marks the easy end of the spectrum:
Clients follow next without treating the choice as an architectural
commitment. The relation is broadly standardized, the target is deliberately
opaque, and the decision rule is tiny: follow while present, stop when absent.
No client has to weigh cancellation against a refund or understand a warehouse.
That success is not an exception to explain away. It reveals the boundary:
Runtime controls are easiest to reuse when their semantics are already shared and choosing them requires little domain judgment.
Opaque continuation tokens, signed download links and server-selected upload targets often share that shape. Their invocation binding changes independently; their meaning does not.
Domain transitions sit further along the same continuum. A cancel control can
still be valuable, but not because it teaches cancellation. Its value comes
from the facts that do vary at runtime: that cancellation is applicable here,
that the server selected this target, and that some arguments may already be
bound.
We can now state the whole result of the browser example. Meaning must be shared; applicability and binding need not be compiled. The remaining question is whether learning those two facts at runtime costs less than the coupling it removes.
#From distinction to price
The distinction creates a temporal trade. A stable operation description is cheap to compile, validate, cache and monitor. A contextual control is current and authoritative, but has to be read, interpreted and revalidated at runtime. If applicability or invocation changes independently of client releases, that price can be worth paying. If neither changes, the control may be ceremony around a method the client already knows.
Part II puts both sides of that ledger on the same page. It starts with an API that publishes both links and a schema, follows the consequences into latency, compatibility and operations, and ends with MCP — a useful counterexample showing that runtime discovery does not have to make the operation vocabulary depend on application state.
#References
-
Roy T. Fielding, Architectural Styles and the Design of Network-based Software Architectures, University of California, Irvine, 2000 — chapter 5, the uniform-interface constraints, client-chosen transitions, and the efficiency trade-off of a uniform interface. https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
-
Roy T. Fielding, “REST APIs must be hypertext-driven,” 2008 — initial URIs, media types, relation names, in-band controls, and the explicit acknowledgement that clients always need shared vocabulary. https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
-
Carson Gross, “HATEOAS is for Humans,” 2016 — the browser, agency and the human component of a uniform client. https://intercoolerjs.org/2016/05/08/hatoeas-is-for-humans.html
-
Carson Gross, “Hypermedia Clients” — why generic mechanics do not supply domain purpose, and why a link-only JSON format is not a complete client protocol. https://four.htmx.org/essays/hypermedia-clients
-
RFC 8288, Web Linking — registered and extension relation types. https://www.rfc-editor.org/rfc/rfc8288
-
IANA Link Relations registry — the standardized relation vocabulary. https://www.iana.org/assignments/link-relations/link-relations.xhtml