وب مدرن، رابط کاربری و تجربه‌ی وب · فصل ۵ — Error Model، REST، Streaming و Webhook · 50

REST API Design: Resources, Commands, Idempotency and Pagination

Design one bounded HTTP API by separating resource state from commands, stating method/idempotency semantics, and defining pagination continuation and error contracts with testable invariants. می‌تواند «REST API Design: Resource, Command, Idempotency, Pagination» را در یک feature محدود طراحی/پیاده‌سازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.

طراحی REST API: Resource، Command، Idempotency و Pagination — fa-IR

Lesson summary

Design one bounded HTTP API by separating resource state from commands, stating method/idempotency semantics, and defining pagination continuation and error contracts with testable invariants.

می‌تواند «REST API Design: Resource, Command, Idempotency, Pagination» را در یک feature محدود طراحی/پیاده‌سازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.

This lesson produces

می‌تواند «REST API Design: Resource, Command, Idempotency, Pagination» را در یک feature محدود طراحی/پیاده‌سازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.

Who it is for

Foundation

Worked baseline, explicit evidence table, safe local fixture and one guided failure probe.

Repair

Reduce to one failing contract, observe evidence, repair, then restore full case.

Experienced

Skip syntax walkthrough; require independent implementation, mutation evidence and residual-risk statement.

Prerequisites

1. First screen: the API that looks fine until a client retries

A job-processing service exposes three obvious operations: list jobs, inspect one job, and ask for a failed job to be retried. The first demo often looks convincing. GET /jobs returns JSON. GET /jobs/J42 returns one object. POST /jobs/J42/retry returns 202 or 201. Then a mobile client times out after sending the retry request and cannot tell whether the server received it. The client repeats the request. Two retry requests are created. A second client walks the list with page numbers while new jobs are inserted at the front; it sees one job twice and never sees another. Nothing here is fixed by choosing prettier endpoint names.

The lesson outcome is narrower and more useful: define a bounded HTTP contract in which resource identity, command intent, repeat-request behavior, continuation behavior, and public errors are explicit enough to test. You will produce HTTP API Contract Decision Record v1. The record must separate what the server promises from what one test happened to observe.

The central question is not “Is this RESTful enough?” It is: Can a client predict the safe way to address state, request a side effect, repeat a request after uncertainty, continue a list, and interpret a failure without inventing guarantees?

By the end, you should be able to defend five claims: stable resource identity comes before URL cosmetics; a side-effecting command may deserve an explicit command boundary; HTTP method idempotency is not the same as application duplicate suppression; pagination requires an ordering and continuation contract; and an HTTP status code is not a complete domain-error model.

2. Route orientation: foundation, repair, experienced

Use the same evidence bar on every route.

Foundation route. Work through the job API and fill one row at a time in the contract matrix. Keep the vocabulary bridge visible: Resource, Command, Safe Method, Idempotent Method, Idempotency Key, Cursor, Pagination, Problem Details, REST. Do not memorize a URL template. For each operation, write the client intent first, then decide whether the intent addresses resource state or asks the server to perform a command.

Repair route. Start with the symptom that failed. Duplicate retry? Focus on application-level duplicate suppression. Duplicates or gaps between pages? Focus on order and cursor semantics. A 404 that means three different things? Focus on the error contract. Repair one dimension, rerun its probe, then restore the full matrix.

Experienced route. Skip the syntax walkthrough, but not the decisions. Produce the matrix, run the duplicate-replay and concurrent-insert probes, explain one alternative you rejected, and complete the changed-context task without copying job-specific names.

A passing implementation is necessary but not sufficient. Completion requires an implementation artifact, observable request/response evidence, a deliberate failure result, repair evidence, changed-context transfer, and one residual limitation.

3. Mental model: intent before endpoint shape

Use this reasoning chain before coding:

client intent → resource or command → HTTP method + URI → repeat semantics → ordering/continuation → response/error → evidence

A resource is a thing the client can identify and reason about as state. A job with identity J42 is a resource. A retry request can also be modeled as a resource if the product wants the client to inspect its lifecycle. A command names an action the client asks the server to perform when that intent is not naturally expressed as replacing a resource representation.

These are modeling choices, not universal URL laws. POST /jobs/J42/retries can be a reasonable command-style boundary because the client requests a new retry attempt for a job. Another system might model retry requests as first-class resources and expose them differently. The quality test is whether identity, side effects, repeat behavior, and errors are clear—not whether a path contains a verb.

Method semantics provide another layer. HTTP defines safety and idempotency at the method-semantic level. A safe method is intended for retrieval and should not be used to request a side effect. An idempotent method has the intended effect of multiple identical requests equivalent to one such request; that does not require byte-identical responses. State, timestamps, headers, or representation details can legitimately differ while the intended effect remains stable.

Application duplicate suppression answers a different question: when a client cannot tell whether a particular side-effecting request succeeded, can it repeat that operation instance without creating a second effect? An Idempotency-Key is one possible contract for that. The key needs a declared scope, a binding to the operation/payload, a replay behavior, and a conflict behavior. Merely accepting a header named Idempotency-Key proves nothing.

Pagination is also a contract, not a UI control. You need an order, a boundary encoded in the continuation token, and a statement about concurrent changes. A cursor can be opaque to the client while still being deterministic on the server. If a traversal is ordered by (createdAt DESC, id DESC), a cursor can represent the last seen tuple; the next page asks for items strictly after/below that boundary according to the declared order. Newer inserts can be excluded from the in-progress traversal without causing duplicates among already-existing items. That is a valid live-list contract if stated honestly. Snapshot pagination would make a different promise and usually needs more state.

4. Foundation layer: five contracts, not five buzzwords

4.1 Resource model

Write stable identities first. For the fixture:

  • Collection resource: GET /jobs
  • Item resource: GET /jobs/{jobId}
  • Retry command boundary: POST /jobs/{jobId}/retries
  • Optional idempotent state replacement example: PUT /jobs/{jobId}/priority

The collection representation should expose enough identity for the client to follow item links or construct item requests from the public contract. The item representation should not leak private storage layout. Do not add endpoint aliases merely to make routing look rich.

4.2 Command boundary

A retry is side-effecting. Hiding it behind GET /jobs/J42/retry makes the public method semantics misleading because clients, caches, crawlers, or diagnostics can treat GET as retrieval. Use a non-safe method for a request whose intent is to create an effect. The exact URI family remains a design choice.

4.3 Idempotency and duplicate suppression

For the lab, the retry command requires Idempotency-Key. The server scopes the key to the retry operation and binds it to the target job. The first request creates one retry request and stores the result under that key. An identical replay returns the same retry request instead of creating another. Reusing the same key for a different target is a conflict.

Notice the precision: this is not a claim that every POST must be non-idempotent or that every API must implement idempotency keys. It is a bounded application contract for an operation where network uncertainty can otherwise create duplicate effects.

The lab also uses PUT /jobs/{id}/priority as a method-level example. Repeating PUT with the same desired state keeps the final priority state the same even if an updatedAt field changes. The intended effect is stable; the response bytes need not be.

4.4 Pagination

Offset pagination can be perfectly adequate for stable or small datasets. It becomes fragile when the dataset changes ahead of the current offset and the client assumes an uninterrupted traversal. Cursor pagination is not automatically correct either. A cursor based on a non-unique or mutable sort key can still duplicate or skip items.

The fixture uses a total order: createdAt DESC, then id DESC. The cursor records the last seen pair. The contract says that jobs inserted newer than the first page boundary are not backfilled into that in-progress traversal. The client starts a new traversal to see the new head. This is a modest promise, but it is testable.

4.5 Error contract

Status codes carry protocol-level meaning, but clients often need a stable public reason inside the response. The fixture uses a Problem Details style object with type, title, status, detail, and a bounded extension code. A missing job and an idempotency-key conflict are not collapsed into one generic string.

Do not treat RFC 9457 as a complete domain taxonomy. It standardizes a problem-details representation; your product still owns the domain meanings and which details are safe to expose.

5. Deep explanation: where plausible shortcuts break

Shortcut A: “REST says nouns only”

This turns a modeling discipline into a naming superstition. The client intent matters more. If “retry this failed job” creates a new retry-request resource, POST /jobs/J42/retries has a clear state transition. A different bounded system could expose an operation resource explicitly. Neither decision is proven by the presence or absence of a verb in the string.

Shortcut B: “POST is non-idempotent, so retries are the client’s problem”

Method semantics do not remove distributed uncertainty. A client may send a request, lose the response, and need a defined retry strategy. If duplicate effects are costly, the application contract must say how the same logical request is recognized. That may be an idempotency key, a client-generated resource identifier, a conditional request, or another bounded mechanism. The lesson uses an idempotency key because it makes the evidence visible.

Shortcut C: “Idempotent means identical response”

Suppose a PUT sets priority=true. The first response may include updatedAt=10:00:01; the second might include updatedAt=10:00:02. The response changed, but the requested final state did not accumulate a second effect. Judge idempotency by the intended server effect of repeated identical requests, not by byte equality.

Shortcut D: “Cursor beats offset”

A cursor is only as strong as its ordering contract. If the cursor contains only createdAt and two jobs share that timestamp, a strict comparison can skip one. If the sort key can change while traversing, an item can move across the boundary. A robust cursor needs a stable total order for the guarantee you are making. The lab uses (createdAt, id) because the pair is immutable and unique inside the fixture.

Shortcut E: “2xx means exactly once”

A success response proves only what the public contract assigns to it and what the client actually received. A lost response can leave the client uncertain. A replay token can suppress duplicate creation in a bounded window, but this lesson does not claim global exactly-once execution across every downstream system. State that residual boundary explicitly.

6. Worked example: job-processing contract matrix

Consider these intents.

Client intentBoundaryMethod / URIRepeat contractPagination / orderPublic failure
List jobscollection resourceGET /jobs?limit=2&cursor=...Safe retrieval(createdAt DESC, id DESC); opaque cursor; newer inserts not backfilledinvalid cursor → problem response
Inspect jobitem resourceGET /jobs/J42Safe retrievaln/aunknown id → 404 problem
Request retrycommand / retry-request creationPOST /jobs/J42/retriesrequires Idempotency-Key; same key + same target replays same logical retry; key reuse with different target conflictsn/amissing key or key conflict → problem response
Set priorityresource-state replacementPUT /jobs/J42/priorityrepeated same desired state has same intended effect; response metadata may differn/aunknown id → 404 problem

The evidence record should not merely copy this table. It should include observed request/response pairs from the local fixture and mark whether each observation supports the contract. One row should show the deliberate broken behavior before repair.

For the pagination probe, request page 1 with two jobs. Record the cursor. Insert a new job that sorts ahead of page 1. Continue with the old cursor. Under the lab contract, the next page must continue strictly below the recorded boundary: no already-seen job appears again, and no original job below the boundary is skipped because of the new head insert. The new job is visible only to a new traversal. That is an intentionally limited guarantee.

For the retry probe, send the same retry request twice with the same Idempotency-Key. The retry-request identifier must remain the same and the stored side-effect count must increase only once. The second response may carry different replay metadata; byte equality is not required.

7. Failure modes and deliberate mutations

Run a prediction before every mutation.

  1. Duplicate command replay. Disable the idempotency lookup. Prediction: two requests with the same key create two retry requests. Evidence: retry count increases twice. Repair: restore operation-key binding and replay the stored logical result.
  2. Unstable pagination. Replace keyset continuation with a positional offset. Prediction: inserting a new head item between page requests shifts positions, causing a duplicate or skip. Repair: restore the declared total-order boundary.
  3. Unsafe GET command. Allow GET /jobs/{id}/retry to trigger a retry. Prediction: a retrieval-style request mutates state. Repair: separate safe retrieval from the command boundary.
  4. Non-idempotent PUT effect. Make the PUT toggle priority instead of setting the requested state. Prediction: repeated identical requests alternate the server state. Repair: make the requested representation/state authoritative.
  5. Weak public error. Replace a structured conflict with 200 {error: ...}. Prediction: protocol status and public domain reason no longer match the contract. Repair: restore the bounded problem representation.

Do not use a mutation merely as a trick for tests. Its purpose is causal evidence: the test should fail because a specific contract dimension was broken, and the repair should restore that dimension without absorbing streaming, webhook-delivery, or framework-architecture scope.

8. Experienced bridge: choose guarantees by cost

Experienced engineers should state the guarantee they need before choosing the mechanism.

For duplicate suppression, ask how long a client may retry, how the key is scoped, whether a request fingerprint is stored, what happens when the same key carries different input, and whether the original response can be replayed. A production system may need persistence and expiration behavior. The lab intentionally keeps this in memory and therefore does not prove durability across process restarts.

For pagination, ask whether the product needs a live traversal, a snapshot, or simply “best effort” navigation. Snapshot semantics can require versioning or retained query state. Live keyset traversal can be much cheaper but may intentionally omit items inserted ahead of the boundary until a new traversal. Offset pagination may be acceptable when data changes are rare or when random page access matters more than stable sequential traversal. There is no universal winner.

For command modeling, ask whether the action has a durable identity the client should inspect. If yes, modeling the command request as a resource can improve observability. If the action is a simple replacement of known state, PUT or PATCH may fit better. Do not create pseudo-resources only to satisfy a slogan.

9. Alternatives and trade-offs

Client-generated resource IDs vs idempotency keys. If the client can choose the identity of a creation request, PUT /retry-requests/{clientId} can make repeat behavior explicit through resource identity. An idempotency key can be easier when the server owns the created resource ID. Both require a collision/conflict policy.

Offset vs keyset cursor. Offset is simple and supports direct page numbers. Keyset continuation can preserve sequential traversal better under certain inserts, but it requires a suitable order and usually gives up arbitrary page jumps. The choice follows the user journey and data-change pattern.

Live traversal vs snapshot. A live contract can tolerate new head inserts not appearing until refresh. A snapshot contract may be necessary for audit/export workflows that require a stable set, but it costs state or versioning. Say which one you implement.

Status-only vs Problem Details style errors. A small internal API may begin with status codes and a tiny error body. A richer public contract benefits from stable machine-readable problem types/codes. Do not expose internal stack traces or storage exceptions as public semantics.

10. Guided practice and independent transfer

Guided pass

Open lab/WEB006-LAB1/learner. Before editing code, fill the “predicted failure” fields in evidence/HTTP_API_Contract_Decision_Record_v1.json.

  1. Run the public structural check.
  2. Inspect the starter’s positional cursor and retry handler.
  3. Define your contract matrix in plain language.
  4. Repair retry duplicate suppression first. Use one operation scope and reject conflicting reuse.
  5. Repair pagination second. Establish the total order before encoding the cursor.
  6. Keep GET side-effect free in the fixture.
  7. Implement the repeated PUT state-setting example.
  8. Record observed responses and one residual limitation.

Independent transfer

Use the transfer file for a media-render task API. The nouns and command differ, but the method stays the same: identify resources, identify one command, define repeat behavior, define list continuation, define public errors, predict a failure, then record evidence. Do not copy job-specific endpoint strings as your reasoning.

The transfer passes only if the decisive implementation and conclusion are yours. AI may suggest checks after you state the contract, but it may not fabricate results or write the final engineering judgment for you.

11. Verification: what counts as evidence

A strong record distinguishes observation from inference.

Observation examples:

  • “Two identical retry submissions with key demo-7 returned retry request R1; repository retry count remained 1.”
  • “After inserting a newer job between page requests, continuation from the old cursor returned the remaining older jobs without repeating the first-page IDs.”
  • “Two identical PUT requests left priority=true; their updatedAt values differed.”

Inference examples:

  • “The bounded fixture suppresses duplicate retry creation for a repeated key while the in-memory key record exists.”
  • “The selected cursor/order prevents this tested head-insert from shifting the continuation boundary.”

Residual limitations:

  • in-memory idempotency state is not durable across restart;
  • the lab does not prove proxy, TLS, network retry, or multi-process behavior;
  • the cursor guarantee is tied to immutable (createdAt,id) ordering;
  • no browser or assistive-technology observation is claimed because this is an API-only fixture.

Evidence is not “the tests are green.” A green test supports a specific claim only when the test exercises the contract boundary and the mutation demonstrates that the test would notice a plausible violation.

12. Assessment and transfer

Retrieval. State the five claim families and one non-goal. A good answer distinguishes method idempotency from application duplicate suppression.

Application. Implement the job API so list/read, retry command, repeat behavior, pagination, and errors match your decision record.

Diagnosis. Predict and run one deliberate mutation. Explain why the observed failure follows from the broken contract, not merely from the test implementation.

Decision. Defend one trade-off: idempotency key vs client-generated request resource, offset vs keyset, or live traversal vs snapshot. State the guarantee you are optimizing.

Transfer. Solve the media-render task case independently. The transfer must preserve the method while changing the domain. If your explanation still depends on job-specific words, your abstraction is not yet stable.

A blocking failure exists if a critical mutation goes undetected, if visual/nominal success is used as the only proof, or if the solution silently steals adjacent lesson scope.

13. References, freshness and claim boundaries

RFC 9110 — HTTP Semantics (S-RFC9110). Used only for bounded method-semantics claims such as safety and idempotency. This lesson does not infer application-level duplicate suppression from method semantics alone.

RFC 9457 — Problem Details for HTTP APIs (S-RFC9457). Used only for the standard problem-details representation. It does not prescribe the complete domain error taxonomy for this job service.

The frozen research snapshot was retrieved on 2026-09-02 and is due for review on 2026-10-17. Query 1 does not expand the source boundary. If a cited standard changes or the review date arrives, narrow or update the affected claim through the governed freshness path.

Final decision record checklist

Before claiming completion, your record must include: the owned contract; a resource/command matrix; method and repeat semantics; pagination order and cursor guarantee; public error shape; at least one predicted mutation; observed failure; repair evidence; independent changed-context transfer; and one residual limitation. It must also state what is not proven: no universal REST naming law, no guarantee that every POST is non-idempotent, no requirement for identical responses from idempotent requests, no claim that cursor pagination is always superior, and no global exactly-once guarantee from a 2xx response.

Assessment

There is no answer key in this lesson pack; answer it yourself and mark honestly. "Needs review" sends the lesson to your review queue.

WEB-006-S1 retrieval

State the five WEB-006 claim families, distinguish method idempotency from application duplicate suppression, and name one explicit non-goal.

lesson ✗AI ✗notes ✗

WEB-006-S2 application

Repair the job fixture so resource identity, retry command semantics, repeat behavior, keyset continuation and public errors match your decision record.

lesson ✗AI ✗notes ✗

WEB-006-S3 diagnosis

Predict, run, diagnose and repair one duplicate-replay, pagination, unsafe-GET, PUT-effect or error-contract mutation.

lesson ✗AI ✗notes ✗

WEB-006-S4 decision

Defend one choice: idempotency key vs client-generated request resource, offset vs keyset, or live traversal vs snapshot. State the guarantee first.

lesson ✗AI ✗notes ✗

WEB-006-S5 transfer

Independently design the media-render task case without copying job-specific endpoint names or conclusions.

lesson ✗AI ✗notes ✗
Assessment rubric

Critical dimensions:

  1. RESOURCE_MODEL — Name stable resource identities and representations before choosing endpoint shapes.
  2. COMMAND_BOUNDARY — Model side-effecting commands explicitly when they are not naturally a simple resource-state replacement.
  3. IDEMPOTENCY — Distinguish HTTP method idempotency from application-level duplicate-suppression contracts.
  4. PAGINATION — Define ordering, continuation token/cursor semantics and behavior under concurrent data changes.
  5. ERROR_CONTRACT — Bind API errors to public semantics without using status codes as the entire domain model.

Blocking assessments: S2, S3 and S5. S1 and S4 are non-blocking but still require explicit scope discipline. Evidence must distinguish HTTP method semantics from application duplicate-suppression, prove deterministic continuation under the stated ordering contract, and keep public error semantics bounded. Publisher-runtime learner execution remains deferred to the pinned runtime. Internal synthetic oracle results are not represented as publisher-runtime observation. Browser/assistive-technology evidence is not claimed.

References & freshness

RFC 9110 — HTTP Semantics
· TOOL_VOLATILE
RFC 9110 defines HTTP method semantics including safety and idempotency; application duplicate-suppression may require additional contracts.
RFC 9457 — Problem Details for HTTP APIs
· TOOL_VOLATILE
RFC 9457 provides a standard problem-details representation without prescribing all domain error designs.

Case studies

Lab files

Goal: repair a local job API contract without network dependencies. The starter is deliberately broken in duplicate command replay, positional pagination, GET side effects, state-setting semantics, and public error shape.

  1. Write the contract before repair.
  2. Run npm test for the public observation surface.
  3. Repair src/job-api.mjs.
  4. Record observations in evidence/HTTP_API_Contract_Decision_Record_v1.json.
  5. Run the listed failure probes and complete the media-render transfer.

The public tests are not the hidden solution oracle. Do not claim network, TLS, proxy, browser, or durability behavior from this local fixture.

Lesson media & slides

MAKNOW_WEB-006_DECISION_EVIDENCE_TABLE_v1.0.0.svg
DECISION EVIDENCE TABLE v1.0.0.svg
MAKNOW_WEB-006_FAILURE_REPAIR_v1.0.0.svg
FAILURE REPAIR v1.0.0.svg
MAKNOW_WEB-006_PRIMARY_MODEL_v1.0.0.svg
PRIMARY MODEL v1.0.0.svg
MAKNOW_WEB-006_WORKED_EXAMPLE_v1.0.0.svg
WORKED EXAMPLE v1.0.0.svg

Teaching files (download)

XLSXHTTP API Contract Decision Record v1 v1.0.0.xlsxTeaching files (download)
DOCXHTTP API Contract Decision Record v1 WORKSHEET v1.0.0.docxTeaching files (download)
PDFHTTP API Contract Decision Record v1 WORKSHEET v1.0.0.pdfTeaching files (download)