Methods are a contract
A method is a promise about what the request does. That promise is not decoration — the entire infrastructure between your client and your handler reads it and acts on it. A CDN will cache a GET. A browser will retry an idempotent request after a dropped connection. A crawler will happily fire GETs at every link it finds. A retry layer will re-send a PUT and refuse to re-send a POST.
GET /admin/users/42/delete. A search-engine crawler indexes the admin panel, follows every link, and deletes the user table. The method was wrong, so every piece of well-behaved infrastructure did exactly the wrong thing correctly.Safe, idempotent, cacheable
- Safe — does not modify server state. Read-only from the caller's point of view. Logging and analytics do not count as modification.
- Idempotent — sending it N times has the same effect as sending it once. Note this is about effect, not response:
DELETEmay return 204 then 404 and still be idempotent. - Cacheable — a response may be stored and reused. GET and HEAD by default; POST only with explicit freshness headers, which almost nobody does.
The full method table
| Method | Safe | Idempotent | Body | Typical use |
|---|---|---|---|---|
| GET | Yes | Yes | No | Fetch a representation |
| HEAD | Yes | Yes | No | Headers only — existence, size, ETag |
| OPTIONS | Yes | Yes | No | Capabilities; CORS preflight |
| POST | No | No | Yes | Create, or any non-uniform action |
| PUT | No | Yes | Yes | Full replace at a known URI |
| PATCH | No | No* | Yes | Partial update |
| DELETE | No | Yes | Discouraged | Remove the resource |
| TRACE | Yes | Yes | No | Loopback diagnostics — disable it |
| CONNECT | No | No | No | Tunnel through a proxy (HTTPS) |
GET
Retrieve a representation. No body — some servers and proxies silently drop it, so a “GET with a JSON body” search API works in curl and breaks behind a CDN. Put filters in the query string; if the filter is genuinely too large for a URL, use POST /searches that returns a search id, then GET the id.
GET /v1/orders?status=shipped&page=2&limit=50&sort=-createdAt
- Keep URLs under ~2000 characters to stay safe across proxies.
- Never mutate on GET, not even a “last viewed” timestamp written synchronously.
- Return
ETagso clients get free 304s.
POST
POST is the general-purpose method: “process this payload according to the resource's own semantics.” It is the correct choice for creating a child resource under a collection, and the correct escape hatch for actions that genuinely are not CRUD.
POST /v1/orders Idempotency-Key: 7c9e-41ab HTTP/1.1 201 Created Location: /v1/orders/8812
Because POST is not idempotent, a client that times out cannot know whether the order was created. The fix is an idempotency key: the server stores the key with the result and replays the same response for a repeat. This is exactly what Stripe does, and it is the answer interviewers are listening for.
Actions that are not CRUD
POST /v1/orders/8812/cancel POST /v1/invoices/91/send POST /v1/videos/12/transcode → 202 Accepted + Location: /jobs/55
Purists will argue for PATCH with a state field. In practice a named action endpoint is clearer, easier to authorise, and easier to rate limit. Use 202 Accepted when the work is asynchronous and hand back a job URL to poll.
PUT and PATCH
PUT replaces the resource at a URI with the payload — the payload is the complete new state. PATCH applies a partial modification. The difference has real consequences for concurrency and for retries, which is why it has its own deep dive in this series.
PUT /v1/users/42
{"name":"Ada","email":"ada@example.com","timezone":"UTC"}
PATCH /v1/users/42
{"timezone":"Europe/London"}PUT is idempotent because the final state does not depend on how many times you sent it. PATCH is idempotent only if the patch is absolute ({"qty": 5}) rather than relative ({"qtyDelta": +1}).
DELETE
Remove the resource. Return 204 No Content when there is nothing to say, or 200 with the deleted representation if the client needs it. A second DELETE returning 404 is fine — idempotency is about state, not status codes — though returning 204 again is friendlier for retry logic.
- Soft deletes: still expose DELETE; the tombstone is an implementation detail.
- Cascading deletes should be explicit —
?cascade=trueor a dedicated endpoint. - Bulk delete has no standard;
POST /v1/orders/bulk-deleteis honest.
HEAD, OPTIONS, TRACE, CONNECT
- HEAD — identical to GET but the response has no body. Useful for checking existence,
Content-Lengthbefore a large download, or whether an ETag changed. - OPTIONS — what can I do here. In practice you meet it as the CORS preflight that browsers send before a cross-origin request with custom headers or a non-simple method.
- TRACE — echoes the request back. Enables Cross-Site Tracing attacks. Turn it off.
- CONNECT — asks a proxy to open a raw TCP tunnel. This is how HTTPS works through a forward proxy. You will never implement it in an application.
OPTIONS /v1/orders Origin: https://app.example.com Access-Control-Request-Method: POST Access-Control-Request-Headers: authorization,content-type HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Methods: GET,POST,PATCH,DELETE Access-Control-Allow-Headers: authorization,content-type Access-Control-Max-Age: 600
Common mistakes
| Mistake | Why it hurts | Fix |
|---|---|---|
| Mutating on GET | Crawlers and prefetchers trigger writes | Use POST/PATCH/DELETE |
| POST for everything (RPC-over-HTTP) | No caching, no safe retries, no semantics for proxies | Model resources; reserve POST for creates and actions |
| PUT for partial updates | Omitted fields silently wiped | PATCH, or require the full body |
| 200 for every response | Clients must parse the body to detect failure | Use the status code as the primary signal |
| DELETE with a required body | Proxies and some clients strip it | Put parameters in the URL |
| Retrying POST blindly | Duplicate orders and double charges | Idempotency-Key with server-side dedupe |
Interview framing
“Reads are GET so the CDN and the browser can cache them with ETags. Creates are POST with an idempotency key, returning 201 and a Location header, because POST is not idempotent and our clients retry on 5xx. Full replacements are PUT, field-level edits are PATCH with an If-Match ETag for optimistic concurrency, and deletes are DELETE returning 204 on repeat calls so retry logic stays simple. Non-CRUD operations get explicit action sub-resources rather than being smuggled into PATCH.”