An order resource returns a cancel link while the order is pending, and stops returning it once the order has shipped. What is the client supposed to gain from that, and what does the client still have to know on its own?
answer
- affordance = what you can do next, computed per state
- state machine lives on the server, not in the client
- link says where, not how to fill the body
- missing link is a hint, never authorisation
- per-user links = per-user cacheability
basics
~20 sThe response advertises the transitions currently available, so the server owns the state machine and the client renders whatever affordances it is given instead of reimplementing the rules. The client still needs to understand each relation's meaning and what payload it takes, and the server still enforces every rule itself.
solid answer
~50 sThe links are affordances: the set of things you can do next from the current state. Because the server computes them, business rules like an order can only be cancelled before it ships live in exactly one place. A UI enables a Cancel button when the cancel relation is present and hides it otherwise, without a copy of the state machine drifting out of sync with the backend. What the client must still know: what each relation means and what body or parameters the transition expects - a link tells you where, not how to fill a form. It also needs its own layout, wording and error handling. What this is not: authorisation. Omitting a link is a UI hint, not a control; the server still rejects the request if it arrives anyway. And because the link set varies per user and per state, responses are user-specific for caching purposes.
code
json · 8 lines{ "id": "42", "status": "PENDING",
"_links": { "self": {"href":"/orders/42"},
"edit": {"href":"/orders/42"},
"https://api.example.com/rels/cancel": {"href":"/orders/42/cancel"} } }
{ "id": "42", "status": "SHIPPED",
"_links": { "self": {"href":"/orders/42"},
"https://api.example.com/rels/track": {"href":"/shipments/9f"} } }go deeper
Say the server tells the client which actions are currently allowed, so the UI shows buttons based on the links present rather than its own rules.
Add that this puts the state machine in one place, that links say where and not how, and that the server still enforces the rule.
Cover the security boundary and shared predicate, caching and Vary implications, the cost of computing affordances, and state-based tests.
Judge where it pays: workflow resources with independently deployed clients yes, lockstep first-party frontends with generated SDKs probably not, and set the rule so it is applied consistently.
## Affordance, defined An affordance is something the representation tells you that you can do right now. In a REST response that is usually a link with a relation name: cancel, edit, pay, refund. The distinguishing feature of a hypermedia-driven design is that the set of affordances is computed by the server per resource and per state, rather than being a fixed list compiled into the client. ## What it actually buys **One home for the state machine.** Whether an order can be cancelled depends on its status, the payment state, maybe the customer's tier. If the client encodes that rule it will duplicate it - and a mobile app cannot be redeployed the day the rule changes. When the rule lives server-side and the client only asks does the response contain the cancel relation, changing the policy is a server change that reaches every client instantly. **Decoupling from URLs.** The client does not build /orders/42/cancel; it follows the href it was given. The server can move the endpoint, route it to another service, or add a signed token to it, and the client keeps working. **Self-describing responses.** A support engineer or a generic console can look at the payload and see what is possible without reading the docs for the state machine. ## What the client still owns A link is where, not how. To use a transition the client needs to know the semantics of the relation (what cancel does, whether it is safe to repeat) and the shape of the request the target expects. Plain links do not describe methods or bodies; formats that carry actions or templates exist for that, but the moment there is a request body, some knowledge sits on the client side unless you adopt one of those richer formats. Layout, copy, confirmation dialogs and error rendering are all client concerns too. ## The security boundary The most common misunderstanding: absence of a link is not enforcement. A client can POST to the cancel URL it saw ten minutes ago, or guess it. The server must apply the same rules on the transition endpoint that it applied when deciding whether to advertise the link, and the two decisions should share one implementation so they cannot diverge. Treat the link set as a hint that keeps honest clients from making pointless calls. ## Operational consequences - **Caching.** If links depend on the user or their permissions, the representation is per-user. That means private caching and correct Vary handling, and it weakens shared caches. - **Cost.** Computing which affordances apply can require extra lookups. If deciding to show refund needs a call to payments, you have just coupled the read path to another service. Keep affordance evaluation cheap or derive it from data you already loaded. - **Testing.** The interesting tests become state-based: for each state, assert the exact link set. That is a good test suite - it pins the state machine - but snapshot tests over whole payloads become churny. ## When it does not pay If every consumer is a generated SDK compiled from a spec, or a single first-party frontend released in lockstep with the API, the indirection buys less and costs payload and complexity. The sweet spot is workflow-shaped resources with real state machines and clients you cannot redeploy on demand.
- Can you drop server-side authorisation checks on the cancel endpoint because you only show the link to users who may cancel?No. The link set is advisory and travels to the client, where it can be inspected, replayed or fabricated. Every transition endpoint must independently enforce the same rule. The right implementation shares one predicate between the link builder and the endpoint guard, so a change cannot update one and forget the other.
- How do you test that affordances are correct?Drive the resource through each state and assert the exact set of relation names present, not just that a specific link exists - forgetting to remove a link is the more dangerous bug. Pair that with an authorisation test proving the endpoint refuses the transition in states where the link is absent.
saying these in an interview costs you the question
- Treating hidden links as an access-control mechanism
- Clients constructing transition URLs by string concatenation while ignoring the links returned
- Duplicating the state machine in the client so both sides must be released together
- Ignoring that per-user link sets make responses uncacheable in shared caches