API contract from the client's side
Agree what the screen can render, recover, continue and reconcile before choosing its endpoint shape.
Predict first
Before you read: predict
Three console reports arrive in order. Their type URIs end in /held, /stock, /held; their titles are “Wait”, “Wait”, “Later”. All have HTTP status 409 and different instance URIs.
Which records belong together in a report grouped by RFC 9457 problem category?
The 30-second version
Design the screen's data, failure, continuation and write receipts together. A successful request can still contain a failed region. Stable machine fields and human explanations serve different consumers. Check what an adapter preserves when responses are partial, extended or replayed. The contract includes those boundary cases, not just a screenshot of the happy path.
Agree what the screen can render, recover, continue and reconcile before choosing its endpoint shape.
How it works
Step 1 of 7
Analogy: A keyed plug meeting its socket
A plug and socket meet through a defined boundary: matching outside dimensions alone does not guarantee that their contacts line up. A connector drawing gives both sides the same positions and labels. Changing the casing while preserving those contact positions differs from moving a contact without changing its label. The receiver needs the agreed interface, not a reconstruction of everything behind it.
| Connector relation | Screen contract |
|---|---|
| Contact positions shared by both drawings | Agreed machine fields and their meaning |
| Distinct contacts within one housing | Independently interpreted parts of an envelope |
| Label on a contact versus a descriptive note | Machine identity versus human explanation |
| An adapter that leaves a contact disconnected | A wrapper that drops a required response field |
In real terms: inspect what actually crosses the boundary. Limits: plugs do not paginate, replay messages or evolve at runtime; those mechanisms require the explicit HTTP examples. Contrasting case: two matching outer shapes can conceal incompatible contact assignments. API styles, cursor construction and boundary validation own their respective implementation details.
Examples, run for real
EXH-1940 → MR !2140: inspect the representation itself
It contains an identity region and a history region. Below is its exact parsed envelope, followed by the nested problem object. The server and consumer assert these fields; they are not abbreviated counts. RFC 9457 §1
E1 envelope {"regions":{"identity":{"data":{"displayName":"DEMO reviewer"}},"history":{"problem":{"type":"https://contracts.example/problems/history-unavailable","title":"history-unavailable","status":503,"detail":"The history service could not answer.","instance":"/incidents/demo-8","retryable":true}}}}
E1 problem {"type":"https://contracts.example/problems/history-unavailable","title":"history-unavailable","status":503,"detail":"The history service could not answer.","instance":"/incidents/demo-8","retryable":true}
| Step | Observable field | Its documented purpose |
|---|---|---|
| 1 | identity.data, history.problem |
Distinguish region representations |
| 2 | type, title, instance |
Separate category, summary and occurrence |
| 3 | retryable |
An application extension, not an RFC-defined guarantee |
| 4 | next:null below |
The fixture's explicit terminal marker |
A terminal audit-history response
The second request returns one audit row and a terminal marker. This example inspects the boundary object; it does not run a traversal algorithm or a checkout repair. The row count and the marker are different fields with separately documented meanings. RFC 9110 §3.2
E2 terminal page {"items":[{"id":"audit-9"}],"next":null}
All examples close their local HTTP server. They prove the declared payloads and consumer observations, not backend authorization, persistent deduplication or a production schema migration. A contract review pairs these observed objects with examples of each failure and replay shape the screen must understand.
Try it
Answer first, then check. Nothing is saved.
Question 1 of 5
A checkout page was built for endpoint X, which answers 503 with one problem object whenever any of its lookups fails. Its renderer:
Endpoint Y replaces X, and the renderer is unchanged. Only the shipping lookup fails, and Y answers 200 with {regions:{totals:{data:{label:'€42'}},shipping:{problem:{type:'https://contracts.example/problems/address-check',title:'address-check'}},loyalty:{data:{label:'14 pts'}}}}.
What does the page show?
const reply = await fetch(route);if (!reply.ok) return show('error');const { regions } = await reply.json();for (const name of ['totals', 'shipping', 'loyalty']) show(`${name}: ${regions[name].data.label}`);Question 2 of 5
A checkout list receives {items:[a], next:'/pages?cursor=opaque-k7'}. The server also has a terminal page [b,c] at that URI.
The client runs the following code with limit=2; what are its final row and HTTP-request counts?
do { page = await get(path); rows.push(...page.items); path = page.next;} while (page.items.length === limit && path);Question 3 of 5
After reload, a checkout has pending operation IDs checkout-a and checkout-b. Their identical POST bodies already created separate orders, but neither response reached the screen. Each retry sends its row's operation ID in the Idempotency-Key request header. Every reply carries an X-Request-Id header and a body { id, replayed }. A wrapper turns each retry's request and its reply's headers and body into a receipt; the screen clears the pending row whose ID equals the receipt's operationId and links /orders/<orderId>.
Which wrapper clears both rows and links the orders the server created?
Question 4 of 5
A form binds its two controls to the submitted body's members shipping.postcode and shipping.street. Its error adapter reads each errors[].pointer as an RFC 6901 JSON Pointer into that body and highlights the control bound to the member the pointer resolves to. Both values failed validation.
Which server projection highlights both controls?
Question 5 of 5
Three deployed clients validate problem responses with Ajv 6 and render pointers only from a body that validates:
The server's next release keeps errors:[{pointer:'/billing/postcode'}] and adds supportTrace:'trace-demo'.
How many postcode highlights appear with X, Y and Z?
const problem = { type: 'object', required: ['type'], properties: { type: { type: 'string' }, title: {}, status: {}, detail: {}, instance: {}, errors: { type: 'array' } } };const X = new Ajv().compile({ ...problem, additionalProperties: false });const Y = new Ajv().compile(problem);const Z = new Ajv({ removeAdditional: true }).compile({ ...problem, additionalProperties: false });const highlights = (body, check) => (check(body) ? body.errors.length : 0);Go deeper
Pro tip
Review one success payload and one failure payload together at the API boundary. Record the documented fields the screen uses, including optional extensions. DOC tip supported by RFC 9457 §3; a contract review itself is not runtime evidence.
Sources
- RFC 9457: problem identity, title, instance, structured extensions, evolution and human detail.
- RFC 9110: resources, representations and methods; it does not prescribe our screen envelope or continuation format.
- RFC 6901: JSON Pointer syntax and evaluation, read 2026-09-30 by direct fetch: “a sequence of zero or more reference tokens, each prefixed by a '/' (%x2F) character.”
- Idempotency-Key draft-07: expired work in progress, pinned to the listed curriculum draft's archived revision. Datatracker confirms its expired status. No adopted-standard claim is made.
Related lessons
Checked against the tools and docs as of 2026-09-30.