We already have the map of resources and their URIs. Now we have to state what can be done with each one, and in REST that is stated by the HTTP method. This lesson walks in depth through GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS applied to Aroma Store, with a complete request and response for each. It then moves on to the two concepts that separate an API which survives reality from one that breaks in production: safety and idempotency. These are not academic subtleties: they are the difference between SwiftShip retrying a request harmlessly and a customer ending up paying twice for the same order. We will close with PUT versus PATCH in depth, soft deletes, idempotency keys and batch operations.

Contents

  1. The methods at a glance
  2. GET: reading without side effects
  3. POST: creating and executing actions
  4. PUT: full replacement
  5. PATCH: partial modification
  6. PUT versus PATCH, and what Aroma Store chooses
  7. DELETE: hard and soft deletes
  8. HEAD and OPTIONS
  9. Safety and idempotency
  10. Idempotency keys for payment
  11. Batch operations

  1. The methods at a glance

Method What it means Body in the request? Body in the response? Safe Idempotent
GET Obtain the representation No Yes Yes Yes
HEAD Like GET, headers only No No Yes Yes
OPTIONS What can be done here No Optional Yes Yes
POST Create a subordinate or execute an action Yes Yes No No
PUT Replace entirely Yes Yes (or 204) No Yes
PATCH Modify partially Yes Yes (or 204) No It depends
DELETE Remove No (normally) Optional (or 204) No Yes

Two more methods exist and we only mention them here: TRACE (echoes the request, disabled for security) and CONNECT (proxy tunnels). No REST API uses them.

If a client uses a method the resource does not support, the correct response is 405 Method Not Allowed with the Allow header; if the server does not know the method at all, it is 501 Not Implemented. The detail of the codes is the next lesson.

  1. GET: reading without side effects

GET obtains the representation of a resource. It is the most used method of any API and the only one that can be cached with guarantees (04-06).

curl -i "https://api.aromastore.example/v1/coffees/cof_001" \
  -H "Accept: application/json"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Language: en
Cache-Control: public, max-age=300

{
  "id": "cof_001",
  "name": "Ethiopia Yirgacheffe",
  "origin": "Ethiopia",
  "roast": "light",
  "priceEuros": 14.50,
  "stock": 120,
  "tastingNotes": ["citrus", "floral", "black tea"],
  "createdAt": "2026-01-15T08:30:00Z",
  "_links": {
    "self": { "href": "/v1/coffees/cof_001" },
    "reviews": { "href": "/v1/coffees/cof_001/reviews" }
  }
}

Design rules for GET in Aroma Store:

  • It never modifies anything. Not even a "discreet" visit counter: browser prefetchers, crawlers and proxies issue GETs of their own accord. If you need to record the visit, do it outside the resource's semantics or with an explicit POST.
  • It carries no body. Technically HTTP does not forbid it, but many intermediaries discard it and some clients do not even send it. Complex queries in a body → POST (02-06).
  • A GET on an empty collection is 200 with {"data": [], "total": 0}, not 404. The collection exists even when it has no elements.
  • A GET on a non-existent element is 404 with the corresponding business error:
HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "error": {
    "code": "coffee_not_found",
    "message": "No coffee exists with the identifier 'cof_999'.",
    "details": []
  }
}

  1. POST: creating and executing actions

POST is the general-purpose method: it creates a resource subordinate to the collection or executes the action that a sub-resource represents (02-02).

3.1. Creating an element in a collection

curl -i -X POST "https://api.aromastore.example/v1/coffees" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Colombia Huila",
    "origin": "Colombia",
    "roast": "medium",
    "priceEuros": 12.90,
    "stock": 80,
    "tastingNotes": ["chocolate", "caramel", "orange"]
  }'
HTTP/1.1 201 Created
Content-Type: application/json
Location: https://api.aromastore.example/v1/coffees/cof_002

{
  "id": "cof_002",
  "name": "Colombia Huila",
  "origin": "Colombia",
  "roast": "medium",
  "priceEuros": 12.90,
  "stock": 80,
  "tastingNotes": ["chocolate", "caramel", "orange"],
  "createdAt": "2026-03-14T09:12:00Z",
  "_links": { "self": { "href": "/v1/coffees/cof_002" } }
}

Three points of the contract:

  1. The identifier is assigned by the server. The client does not send id; if it does, the request is rejected with 400.
  2. Location is mandatory on every creation. It contains the URI of the created resource. It is what lets a client chain operations without guessing URLs.
  3. The complete resource is returned in the body, not just the id: it saves an immediate GET and shows the fields calculated by the server (createdAt).

3.2. Executing an action

curl -i -X POST "https://api.aromastore.example/v1/orders/ord_5001/payment" \
  -H "Authorization: Bearer <token>" \
  -H "Idempotency-Key: 5f3b9c2a-1d7e-4a44-9f30-8b1c2d3e4f50" \
  -H "Content-Type: application/json" \
  -d '{ "method": "card", "cardToken": "tok_visa_4242" }'
HTTP/1.1 201 Created
Content-Type: application/json
Location: https://api.aromastore.example/v1/orders/ord_5001/payment

{
  "status": "paid",
  "amountEuros": 29.00,
  "method": "card",
  "reference": "pay_7712",
  "paidAt": "2026-03-14T10:32:00Z",
  "_links": {
    "self": { "href": "/v1/orders/ord_5001/payment" },
    "order": { "href": "/v1/orders/ord_5001" },
    "invoice": { "href": "/v1/orders/ord_5001/invoice" }
  }
}

3.3. POST is not idempotent

It is the characteristic that defines POST and the source of most real-world incidents:

# Executed twice, it creates TWO different coffees
curl -X POST .../v1/coffees -d '{"name": "Colombia Huila", ...}'   # → cof_002
curl -X POST .../v1/coffees -d '{"name": "Colombia Huila", ...}'   # → cof_003

When that is unacceptable (payments, orders), there are two remedies: the Idempotency-Key header (section 10) or returning 409 Conflict if you detect a business duplicate. Aroma Store uses both.

  1. PUT: full replacement

PUT says: "exactly this representation must end up at this URI". It is a complete replacement, not a merge.

curl -i -X PUT "https://api.aromastore.example/v1/coffees/cof_001" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ethiopia Yirgacheffe",
    "origin": "Ethiopia",
    "roast": "light",
    "priceEuros": 15.20,
    "stock": 120,
    "tastingNotes": ["citrus", "floral", "black tea"]
  }'
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "cof_001",
  "name": "Ethiopia Yirgacheffe",
  "origin": "Ethiopia",
  "roast": "light",
  "priceEuros": 15.20,
  "stock": 120,
  "tastingNotes": ["citrus", "floral", "black tea"],
  "createdAt": "2026-01-15T08:30:00Z"
}

The danger of PUT: if the client wants to raise the price and sends only {"priceEuros": 15.20}, a correct PUT leaves the coffee with no name, no origin, no roast, no stock and no tasting notes. It is the most expensive rookie mistake in this lesson. Aroma Store mitigates it by rejecting with 400 any PUT that is missing mandatory fields, but the semantics are still "replace everything".

PUT is idempotent: sending it ten times leaves the resource exactly as sending it once does. That is why it is the method SwiftShip uses to update the shipment, whose retries are frequent:

PUT /v1/orders/ord_5001/shipment HTTP/1.1
Content-Type: application/json
Authorization: Bearer <SwiftShip's token>

{
  "status": "out_for_delivery",
  "trackingNumber": "SS-9981234",
  "estimatedDelivery": "2026-03-16T12:00:00Z"
}

A little-known detail: PUT can also create a resource if the client knows the URI in advance. Aroma Store uses this in exactly one place, cart items:

PUT /v1/carts/crt_77/items/cof_002 HTTP/1.1
Content-Type: application/json

{ "quantity": 3 }

If the item did not exist it is created (201 Created); if it did exist the quantity is replaced (200 OK). And it is idempotent: pressing "set 3 units" three times leaves 3 units, not 9. Compare it with POST /items carrying {"coffeeId": "cof_002", "quantity": 1}, which adds one unit every time it runs: both operations make sense, but they mean different things and it is worth having that written down in the documentation.

  1. PATCH: partial modification

PATCH sends only what changes. The interesting question is in what format, because PATCH does not define one: the Content-Type does.

5.1. JSON Merge Patch (RFC 7386)

The document sent is merged with the resource. A null value means "delete this field".

PATCH /v1/coffees/cof_001 HTTP/1.1
Content-Type: application/merge-patch+json

{
  "priceEuros": 15.20,
  "stock": 95
}

Result: priceEuros and stock change; everything else stays as it was.

PATCH /v1/coffees/cof_001 HTTP/1.1
Content-Type: application/merge-patch+json

{ "tastingNotes": null }

Result: the tastingNotes field is removed.

Merge Patch's limitation is arrays: they are replaced whole, never edited by position. To add a tasting note you have to send the complete list:

{ "tastingNotes": ["citrus", "floral", "black tea", "bergamot"] }

And the awkward corollary: with Merge Patch you cannot genuinely set a field to null, because null is reserved for "delete".

5.2. JSON Patch (RFC 6902)

It is a list of operations on the document, expressed with JSON Pointer.

PATCH /v1/coffees/cof_001 HTTP/1.1
Content-Type: application/json-patch+json

[
  { "op": "replace", "path": "/priceEuros", "value": 15.20 },
  { "op": "add",     "path": "/tastingNotes/-", "value": "bergamot" },
  { "op": "remove",  "path": "/tastingNotes/0" },
  { "op": "test",    "path": "/stock", "value": 120 }
]

The operations are add, remove, replace, move, copy and test. path uses JSON Pointer (/tastingNotes/- means "at the end of the array"). If any operation fails, none of them is applied: it is atomic.

The test operation is the hidden gem: "apply this only if stock is still 120". It is optimistic concurrency control inside the body itself, complementary to the one done with If-Match and ETags (04-06).

5.3. Comparison

Criterion JSON Merge Patch (7386) JSON Patch (6902)
Content-Type application/merge-patch+json application/json-patch+json
Readability Very high: it looks like the resource Low: you have to read operations
Editing one array element No (the array is replaced) Yes, by index
Setting a field to null Impossible (null = delete) Yes (replace with value: null)
Preconditions No Yes, with test
Reorder, move, copy No Yes
Idempotency Yes, always Not always (add to /array/- accumulates)
Ease for the client Very high Medium
Adoption Mainstream Niche (complex documents, Kubernetes)

5.4. Aroma Store's decision

JSON Merge Patch as the official format, with Content-Type: application/merge-patch+json. Reasons: the resources are flat and small, the clients are mostly frontends that already have the object in memory, the readability of the requests makes support easier, and 100% of the real use cases are "change two or three fields". Aroma Store's arrays (tastingNotes, items) are short and replacing them whole is not a problem.

On top of that, plain application/json is accepted and treated as Merge Patch, because that is what many tools send by default; a Content-Type other than those two is rejected with 415 Unsupported Media Type. The fact that the API does not support JSON Patch is written down in the documentation, so that nobody tries.

  1. PUT versus PATCH, and what Aroma Store chooses

Criterion PUT PATCH
Semantics Replaces the complete resource Applies partial changes
Absent fields Deleted or set to their default Kept
Body size The whole resource Only what changes
Idempotent Always With Merge Patch, yes
Risk of overwriting someone else's changes High: you send fields you did not mean to touch Low: you only touch your own
Can create the resource Yes No (404 if it does not exist)
Typical use Complete forms, synchronisation One-off edits

The assignment in Aroma Store's map:

Resource Update method Reason
/coffees/{id} PUT and PATCH The panel edits complete records; price and stock adjustments are partial
/customers/{id} PATCH only A customer is never replaced whole: there are fields the customer does not see
/customers/{id}/preferences PUT only There are four fields and the form sends all of them
/carts/{id}/items/{coffeeId} PUT only Setting the quantity must be idempotent
/orders/{id} PATCH only Only very specific fields are editable (the address before shipping)
/orders/{id}/shipment PUT only SwiftShip sends the complete state and retries
/reviews/{id} PATCH only The comment gets corrected; the status is changed with /approval or /rejection

  1. DELETE: hard and soft deletes

curl -i -X DELETE "https://api.aromastore.example/v1/carts/crt_77/items/cof_002" \
  -H "Authorization: Bearer <token>"
HTTP/1.1 204 No Content

7.1. Hard versus soft

Hard delete Soft delete
What it does Removes the row Marks deleted = true and hides it
Reversible No Yes
Auditing and history Lost Preserved
Referential integrity May break old orders Intact
Cost None Filtering in every query, table growth

Aroma Store uses soft deletes for coffees (an order from 2025 must still be able to show the coffee that was sold) and for customers (tax obligations), and hard deletes for cart items (ephemeral data with no historical value). Orders are never deleted: they are cancelled with POST /orders/{id}/cancellation.

Key point: the soft delete is invisible in the contract. From the outside, after a DELETE /v1/coffees/cof_001, the coffee no longer appears in /v1/coffees and GET /v1/coffees/cof_001 responds 404 (or 410, see below). That the row is still there internally is the persistence layer's business (03-05).

7.2. What if I delete twice?

Here is a classic debate. A second DELETE on an already deleted resource:

  • 404 Not Found: literal. The resource does not exist now, and this is the most common answer.
  • 204 No Content: pragmatic. The client's goal ("that it should not exist") is already met.

Neither of the two breaks idempotency, and it is worth understanding why: idempotency means that the effect on the server is the same after one or N requests, not that the response code is identical. After one or five DELETEs, the resource is deleted: that is idempotent.

Aroma Store's decision: 404, because it distinguishes "I deleted it just now" from "this was already gone", useful information for debugging clients with retries. And for coffees permanently withdrawn from the catalogue, 410 Gone is used, which says "it existed and will not be back" (02-04).

DELETE should not carry a request body. If you need parameters in order to delete (a reason, an effective date), it is a sign that you are dealing with an action, and actions are modelled as a sub-resource with POST (02-02).

  1. HEAD and OPTIONS

8.1. HEAD

Identical to GET, but the server returns only the headers. It is useful for checking existence, size or freshness without downloading the body.

curl -I "https://api.aromastore.example/v1/orders/ord_5001/invoice"
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 48213
Last-Modified: Sat, 14 Mar 2026 10:33:00 GMT

Aroma Mobile uses it to work out whether it is worth downloading a 48 KB invoice over the current connection. The golden rule: HEAD must return exactly the same headers GET would return; if your HEAD returns Content-Length: 0, it is badly implemented.

8.2. OPTIONS

It asks what can be done with a resource. The mandatory part of the response is the Allow header.

curl -i -X OPTIONS "https://api.aromastore.example/v1/coffees/cof_001"
HTTP/1.1 204 No Content
Allow: GET, HEAD, PUT, PATCH, DELETE, OPTIONS
Accept-Patch: application/merge-patch+json

Accept-Patch is the standard way of announcing which PATCH formats the resource accepts: the decision from section 5.4 is published here in the protocol itself.

The heavy use of OPTIONS in practice does not come from people but from browsers: it is CORS's preflight request, which the Aroma Store SPA fires before every PATCH or DELETE that carries custom headers. That whole mechanism is studied in 04-05.

  1. Safety and idempotency

The two most important properties of this lesson.

  • Safe: the method does not modify server state. It is a read. Any intermediary may repeat it, prefetch it or cache it without asking permission.
  • Idempotent: executing it N times leaves the server in the same state as executing it once. Careful: the same state, not the same response.
graph TD
    A["Client sends POST /orders/ord_5001/payment"] --> B["The server receives it<br/>and charges €29.00"]
    B --> C["The response is lost:<br/>network timeout"]
    C --> D{"Does the client retry?"}
    D -->|"Without Idempotency-Key"| E["A second charge of €29.00<br/><b>customer charged twice</b>"]
    D -->|"With Idempotency-Key"| F["The server recognises the key<br/>and returns the original response<br/><b>a single charge</b>"]

Every safe method is idempotent; the converse is not true (DELETE is idempotent but not safe).

Why it really matters, with Aroma Store's three scenarios:

  1. SwiftShip's retries. Their HTTP client retries automatically on a 503 or a timeout. Since it updates the shipment with PUT (idempotent), three retries leave the same shipment. Had we modelled it as POST /orders/{id}/shipment-events, we would have three duplicate events.
  2. Network timeouts on mobile. Aroma Mobile loses signal right after sending the request. The client does not know whether the server processed it. It can only retry without fear if the method is idempotent.
  3. Double-clicking "pay". The most human case of all. POST is not idempotent, so the remedy cannot come from the method: it comes from the next section.

Design consequence: the more idempotent your API is, the cheaper it is to operate, because automatic retries (from the client, the load balancer, the gateway) stop being dangerous.

  1. Idempotency keys for payment

An idempotency key is a unique identifier the client generates before sending the request and repeats on every retry of that same logical operation.

The complete flow for /orders/ord_5001/payment:

# The client generates a UUID and stores it BEFORE sending anything
KEY="5f3b9c2a-1d7e-4a44-9f30-8b1c2d3e4f50"

curl -i -X POST "https://api.aromastore.example/v1/orders/ord_5001/payment" \
  -H "Authorization: Bearer <token>" \
  -H "Idempotency-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{ "method": "card", "cardToken": "tok_visa_4242" }'

What the server does:

  1. It looks the key up. If it does not exist, it records it together with a fingerprint of the body, processes the payment and stores the response for 24 hours.
  2. If it exists and the body matches: it does not charge again; it returns the stored response, with Idempotent-Replay: true so the client knows it is a repeat.
  3. If it exists and the body is different: it responds 422 Unprocessable Content with the code idempotency_key_reused. This is protection against client bugs: the same key cannot mean two different operations.
  4. If the original request is still being processed: it responds 409 Conflict with operation_in_progress and Retry-After: 2.

The retry's response:

HTTP/1.1 201 Created
Content-Type: application/json
Location: https://api.aromastore.example/v1/orders/ord_5001/payment
Idempotent-Replay: true

{
  "status": "paid",
  "amountEuros": 29.00,
  "reference": "pay_7712",
  "paidAt": "2026-03-14T10:32:00Z"
}

Aroma Store's contract regarding Idempotency-Key:

Endpoint Key? Behaviour without a key
POST /orders Mandatory 400 with idempotency_key_required
POST /orders/{id}/payment Mandatory 400 with idempotency_key_required
POST /orders/{id}/cancellation Recommended It is processed; if it is already cancelled, 409 order_already_cancelled
POST /coffees Optional It is processed (it could create duplicates)
POST /coffees/{id}/reviews Optional It is processed

And a second line of defence that does not depend on the client: the resource itself protects its transition. A second payment for the same order, even with a different key, finds the order in the paid state and responds 409 Conflict with order_already_paid. Idempotency protects you from technical retries; the state machine protects you from logical errors. You need both.

  1. Batch operations

Sooner or later somebody will ask to "update the stock of 200 coffees in one go". The options:

a) N individual requests. Semantically perfect, easy to cache and to retry. With HTTP/2 and multiplexed connections, 200 small PATCHes are far more viable than people assume.

b) A batch endpoint.

POST /v1/coffees/bulk-updates HTTP/1.1
Content-Type: application/json

{
  "operations": [
    { "id": "cof_001", "changes": { "stock": 95 } },
    { "id": "cof_002", "changes": { "stock": 0 } },
    { "id": "cof_999", "changes": { "stock": 10 } }
  ]
}

And here the fundamental problem of batching appears: what code do you return if one of the three fails? Not 200, because something failed. Not 400, because two succeeded. The usual answer is 207 Multi-Status, a code that comes from WebDAV, with the detail per element:

HTTP/1.1 207 Multi-Status
Content-Type: application/json

{
  "data": [
    { "id": "cof_001", "status": 200 },
    { "id": "cof_002", "status": 200 },
    { "id": "cof_999", "status": 404,
      "error": { "code": "coffee_not_found", "message": "'cof_999' does not exist.", "details": [] } }
  ],
  "total": 3
}

Risks to bear in mind before accepting a batch endpoint:

  • Ambiguous atomicity: is it all-or-nothing, or partial? You have to decide and document it; both options are defensible and it is the confusion that does the damage.
  • You lose caching and Location: it is an opaque POST to an artificial endpoint.
  • Complicated idempotency: retrying the batch after a partial failure requires an idempotency key and knowing what was applied.
  • Timeouts and limits: a batch of 10,000 elements brings the request down; you have to set a maximum (Aroma Store: 100 operations) and respond 413 if it is exceeded.
  • A verb in disguise: bulk-updates is an invented resource that does not exist in the domain. It is a conscious concession, not a pattern to extend.

Aroma Store's decision: there are no batch endpoints in v1. The internal panel updates in parallel with individual requests. If volume demands it, one single batch endpoint for stock will be added, with the above rules written into the style guide.

Common Mistakes and Tips

  • Using GET for operations that modify. GET /delete?id=rev_101 is a disaster waiting for a crawler. Never.
  • Using POST for everything. It works, but you give up caching, safe retries and half of HTTP's semantics: it is the Richardson level 1 we already rejected in 01-05.
  • Sending a partial PUT. The classic mistake that wipes out half a resource. If you are going to send three fields, use PATCH.
  • Returning 200 on creation. Creation is 201 with Location. A 200 forces the client to dig the id out of the body.
  • Implementing PATCH without deciding the format. Without an explicit Content-Type, every client will assume something different. Document application/merge-patch+json and reject the rest with 415.
  • Believing that idempotency means "returns the same thing". It means "leaves the server the same". A second DELETE may respond 404 and still be idempotent.
  • Putting a body in GET or DELETE. Some intermediaries discard it silently and debugging that is a nightmare.
  • Tip: always ask "what happens if this gets sent twice?". Apply it to every new endpoint before calling it done. It is the question that prevents the most incidents.
  • Tip: the idempotency key is generated by the client before sending, not afterwards. If it is generated on the retry, it is useless.

Exercises

Exercise 1: choose the method

State the method, the URI and the reason, for each Aroma Store need:

  1. Aroma Mobile wants to know whether the invoice for ord_5001 is available yet, without downloading it.
  2. The panel corrects a typo in the name of cof_002.
  3. The SPA sets the quantity of cof_002 in cart crt_77 to 3 units.
  4. A moderator rejects review rev_102, stating the reason.
  5. SwiftShip reports that ord_5001 has gone out for delivery.
  6. The panel withdraws cof_001 from the catalogue while preserving the history.
  7. A customer confirms their cart and creates an order.

Exercise 2: PUT versus PATCH

This is the current state of cof_001:

{
  "id": "cof_001",
  "name": "Ethiopia Yirgacheffe",
  "origin": "Ethiopia",
  "roast": "light",
  "priceEuros": 14.50,
  "stock": 120,
  "tastingNotes": ["citrus", "floral", "black tea"]
}

a) What is left after PUT /v1/coffees/cof_001 with the body {"priceEuros": 15.20}, if the server does not validate mandatory fields? b) Write the Merge Patch PATCH that raises the price to €15.20 and lowers the stock to 95. c) Write the Merge Patch PATCH that removes the tastingNotes field. d) Write the JSON Patch that adds the note "bergamot" only if the stock is still 120, and explain why that cannot be done with Merge Patch.

Exercise 3: design the idempotency of a return

Aroma Store is adding POST /v1/orders/{id}/return, which generates a return label and refunds the amount. Design its behaviour by answering:

  1. Should it require Idempotency-Key? Why?
  2. What happens if the same key arrives twice with the same body?
  3. What happens if a return arrives for an order that has not shipped yet?
  4. What happens if a second return arrives, with a different key, for an order that has already been returned?
  5. Would modelling it as PUT /v1/orders/{id}/return be idempotent? What would you gain and what would you lose?

Solutions

Solution 1

# Method and URI Justification
1 HEAD /v1/orders/ord_5001/invoice Checks existence and size without spending data: exactly what HEAD exists for
2 PATCH /v1/coffees/cof_002 with {"name": "..."} A partial change; with PUT you would have to resend the whole record and risk overwriting other fields
3 PUT /v1/carts/crt_77/items/cof_002 with {"quantity": 3} "Set the quantity" is a replacement and idempotent; with POST it would add up each time
4 POST /v1/reviews/rev_102/rejection with {"rejectionReason": "..."} An action with parameters of its own, modelled as a sub-resource (02-02)
5 PUT /v1/orders/ord_5001/shipment with the complete state A singleton updated by a partner that retries: PUT's idempotency is essential
6 DELETE /v1/coffees/cof_001 A soft delete internally; from the outside the coffee disappears from the catalogue
7 POST /v1/orders with Idempotency-Key Creation in the collection, not idempotent by nature: the key avoids duplicate orders

Solution 2

a) A literal PUT replaces the complete resource, so what would be left is:

{ "id": "cof_001", "priceEuros": 15.20 }

No name, no origin, no roast, no stock and no tasting notes. The id survives because it is part of the identity, not of the content sent. That is why Aroma Store validates the mandatory fields and returns 400 invalid_data instead of destroying the resource.

b)

PATCH /v1/coffees/cof_001 HTTP/1.1
Content-Type: application/merge-patch+json

{ "priceEuros": 15.20, "stock": 95 }

c)

PATCH /v1/coffees/cof_001 HTTP/1.1
Content-Type: application/merge-patch+json

{ "tastingNotes": null }

d)

PATCH /v1/coffees/cof_001 HTTP/1.1
Content-Type: application/json-patch+json

[
  { "op": "test", "path": "/stock", "value": 120 },
  { "op": "add",  "path": "/tastingNotes/-", "value": "bergamot" }
]

With Merge Patch it is impossible for two cumulative reasons: there is no conditional operation (test), and arrays are replaced whole, so "append at the end" forces you to send the complete list —which, on top of that, would overwrite any note added by another user between the read and the write. Aroma Store's alternative for the conditional case is not JSON Patch but If-Match with an ETag (04-06).

Solution 3

  1. Yes, mandatory. It moves money: a retry after a timeout cannot cause two refunds. Same criterion as /payment.
  2. No second refund is issued. The server returns the stored response from the first execution with Idempotent-Replay: true and the same 201 and Location.
  3. 409 Conflict with a new business code, order_not_shipped: the order exists and the request is well formed, but its current state does not allow the transition. It is not 400 (the data is valid) nor 404 (the order exists).
  4. 409 Conflict with order_already_returned. The new key does not help: it is the resource's state machine that rejects the second transition. This is the example of why both defences from section 10 are needed.
  5. It would indeed be idempotent, and that is its appeal: PUT /return would mean "I want this return to exist with this data", and repeating it would leave the same state. You would gain idempotency without an extra header. You would lose, on the other hand, the semantics of an action with side effects (a PUT suggests that a piece of data is being written, not that a refund is being executed), the possibility for the server to assign data belonging to the operation (the refund reference, the date) and consistency with /payment, /cancellation and /approval, which already use POST. Aroma Store prioritises consistency: POST with an idempotency key.

Conclusion

HTTP methods are your API's vocabulary of verbs and using them well is what separates Richardson level 1 from level 2. GET reads without side effects and can be cached; POST creates and executes actions, and is not idempotent; PUT replaces entirely and is idempotent; PATCH modifies only what is needed —in Aroma Store with JSON Merge Patch and Content-Type: application/merge-patch+json—; DELETE removes, even if internally it is a soft delete; and HEAD and OPTIONS give information without transferring the resource. Above all of that stand safety and idempotency, which stop being theory the moment there are retries, timeouts or a double click: that is why payment and order creation require an Idempotency-Key and, in addition, the resource's state machine rejects impossible transitions.

That is precisely where we have been leaving loose ends: 201 with Location, 204 with no body, 404 versus 410, 409 when the transition is not possible, 415 when the Content-Type is not supported, 405 when the method is not allowed. Each of those numbers is a contract decision. In the next lesson, 02-04 HTTP Status Codes, we will go through all of them with judgement, build a decision tree for choosing the right one, design Aroma Store's error body against the standard application/problem+json and close out the catalogue of business error codes.

REST API Course: Principles of Designing and Developing RESTful APIs

Module 1: Introduction to RESTful APIs

Module 2: Designing RESTful APIs

Module 3: Building RESTful APIs

Module 4: Best Practices and Security

Module 5: Tools and Frameworks

Module 6: Case Studies and Projects

© Copyright 2026. All rights reserved