"Stop translating English verbs into HTTP methods. 'Create' isn't always POST; 'update' isn't always PUT. The choice is driven by the three invariants — safety, idempotency, cacheability — not by the dictionary."
The Decision Tree
For any operation you're designing, walk this tree top-to-bottom:
1. Will this change any observable server state?
NO → use GET (or HEAD if you only need headers, OPTIONS to discover methods)
YES → continue to 2
2. Does the client know the exact target URI?
NO → use POST (server assigns the URI; returns 201 + Location)
YES → continue to 3
3. Are you sending the FULL representation of the resource?
YES → use PUT (idempotent: same payload → same state)
NO → use PATCH (a partial update; carries a diff/delta)
4. Removing the resource entirely?
→ use DELETE
5. Is this an operation with no obvious target resource (a 'command' style)?
→ use POST on an action-named URI (e.g. POST /charges/42/refund)
The Method/Outcome Matrix
Most operations fit one of these patterns:
| Intent | Method | URI shape | Status code on success |
|---|---|---|---|
| Read a resource | GET | /users/42 | 200 OK (or 304 if cached) |
| List resources | GET | /users?role=admin | 200 OK |
| Test existence cheaply | HEAD | /users/42 | 200 OK (no body) |
| Discover allowed methods | OPTIONS | /users/42 | 200 OK + Allow header |
| Create with server-assigned ID | POST | /users (collection) | 201 Created + Location |
| Create with client-assigned ID | PUT | /users/client-picked-id | 201 Created |
| Replace existing resource | PUT | /users/42 | 200 OK (or 204) |
| Partial update | PATCH | /users/42 | 200 OK |
| Remove | DELETE | /users/42 | 204 No Content |
| Async / long-running | POST | /jobs (collection) | 202 Accepted + Location |
| Command / action | POST | /charges/42/refund | 200 OK or 202 |
| Upload a file | POST | /uploads | 201 Created |
Three Worked Examples
"Send a welcome email to user 42." No obvious target resource for the email itself; the operation has side effects (the email goes out); not idempotent (sending twice sends two emails). POST on an action URI: POST /users/42/welcome-emails or POST /emails with a body specifying the recipient. Add an Idempotency-Key header if you want safe retries.
"Update user 42's email address." Has a target URI; the client knows the resource; this is a partial update. PATCH with {"email": "new@example.com"}. Or PUT with the full user payload if your API is PUT-everything-style (some are).
"Refund payment 42." No obvious natural resource for "a refund operation"; the operation has side effects; almost certainly NOT idempotent without coordination. POST on an action URI: POST /payments/42/refunds. Add Idempotency-Key. The response could be 201 (a Refund resource was created) or 200 (operation succeeded).
The 'Tunnel Everything Through POST' Anti-Pattern
Some teams default every endpoint to POST. "It always works. We don't have to think." The costs are real:
- CDNs can't cache the responses (POST is not cacheable by default).
- Clients can't safely retry on transport failures (POST is not idempotent).
- Browsers and HTTP intermediaries can't optimize (no speculative prefetch, no conditional GET).
- Logs and dashboards lose the diagnostic signal — every operation looks like "POST something happened."
- OpenAPI / Swagger docs become a wall of POSTs with no shape.
The protocol gives you seven verbs precisely so this information is in the wire. Throwing it away makes the system less observable, less cacheable, less retryable. Don't do it.
cwkPippa's Method Choices
backend/routes/ and you'll see deliberate choices everywhere. POST /api/conversations creates with a server-assigned ID; GET /api/conversations/{id} reads (with the healing layer transparent to the client); PUT /api/conversations/{id}/title renames (idempotent — same name → same state); PATCH /api/folders/{id} changes color or display name; DELETE /api/conversations/{id} removes; POST /api/council/{id}/finalize is a command (not idempotent — finalizing twice would be a bug). Every one of these picks comes from walking the tree.