"Every tutorial tells you 'POST creates, PUT updates.' That's the consequence. The rule is idempotency — and once you see the rule, the verb mapping picks itself."
Three Yes/No Questions That Define Every Method
RFC 9110 defines three semantic properties. Every method either has each property or doesn't. The combination is the method's contract with the entire HTTP ecosystem — clients, proxies, CDNs, retry logic.
- Safe — the request makes no observable change to server state. Reading is safe; writing is not. Safe requests can be auto-retried, prefetched, or speculatively executed.
- Idempotent — sending the same request N times leaves the server in the same state as sending it once. (Note: state, not response. A DELETE returns 204 the first time and 404 the second time — different responses, same final state.)
- Cacheable — the response is allowed to be stored and reused by intermediaries. Some methods are cacheable by default; some only with explicit headers.
The Method Matrix
Memorize this once and most arguments about "is this RESTful?" dissolve:
Method | Safe | Idempotent | Cacheable (default)
---------|------|------------|--------------------
GET | yes | yes | yes
HEAD | yes | yes | yes
OPTIONS | yes | yes | rarely (technically yes)
PUT | no | yes | no
DELETE | no | yes | no
POST | no | no | only if response says so
PATCH | no | no (usually) | no
Two things to notice. First: the only methods that change state AND are idempotent are PUT and DELETE. That's the entire shape of "safe retry on write." Second: POST is the wildcard — neither safe nor idempotent — because it's the catch-all "do whatever this endpoint does." That openness is what makes POST useful and what makes naive POST clients dangerous.
Why Production Cares
Three real-world consequences of getting this right (or wrong):
1. Transport retries. A request crosses 10 routers and reaches the server. The server processes it. The response packet gets dropped on the way back. The client times out and doesn't know: did the server see it? Idempotent methods let the client safely retry — at worst, the server processes it again with no different end state. Non-idempotent methods make retry dangerous: a payment, a message-send, a council-finalize. The wrong retry charges the card twice.
2. CDN and proxy behavior. Caches aggressively serve cached responses for GET. They will NEVER cache POST without explicit opt-in (and most still won't). They sometimes pre-fetch links speculatively, but only for GET — fetching a DELETE "in advance" would be catastrophic. The protocol lets caches do this safely because the methods promise the right invariants.
3. Optimistic locking. PUT being idempotent enables If-Match: "<etag>" — "only apply this PUT if the resource hasn't been updated since I read it." Without idempotency, this dance would be incoherent. Track 2 lesson 4 details the cache contract; the foundation is here.
The Idempotency-Key Pattern (POST's Workaround)
POST isn't idempotent — but business operations often need to be. The widely-adopted solution: the client generates a unique Idempotency-Key header for each logical operation. The server stores keys it has seen and returns the same response for any repeat of the same key. Stripe popularized this; it's now standard for any POST you want safe to retry (payments, account creation, anything with side effects).
cwkPippa's Reality
POST /api/chat is genuinely not idempotent — sending the same message twice creates two assistant responses (which costs Claude API tokens twice). The frontend mitigates with debouncing and a UUID per send-button-click, but the wire-level POST has no Idempotency-Key yet. That's a known gap; the day it bites us in production is the day I add it. Meanwhile, every PUT/DELETE in backend/routes/ is safe to retry because they were designed idempotent. The asymmetry is by design.