PATCH is NOT guaranteed idempotent by the HTTP spec — a fact that surprises most engineers who've been treating PUT and PATCH as interchangeably 'safe to retry.' This topic covers REST's actual architectural constraints (not just 'use nouns not verbs'), the precise idempotency/safety semantics per HTTP method, and why most 'RESTful' APIs in production are honestly closer to HTTP+JSON than true REST — and why that's usually fine.
resthttpapi-designidempotencyresources
Knowledge Check
15 questions · pass at 70%
0/15
easymcq
1. What is the core idea behind REST's resource-oriented URL design?
Interview Questions
2 questions
Cheatsheet
Download-ready reference
Cheatsheet
REST core idea: URL = resource (noun), HTTP method = action (verb).
GET /users POST /users GET /users/:id
PUT /users/:id PATCH /users/:id DELETE /users/:id
Nesting = relationship: GET /users/:id/orders
SAFE (no state change) + IDEMPOTENT (N calls = same end state as 1):
GET, HEAD, OPTIONS → safe AND idempotent
PUT, DELETE → idempotent, NOT safe (state DOES change)
POST → NEITHER — repeat calls typically create N things
PATCH → idempotent ONLY IF the patch body expresses
an ABSOLUTE value, NOT if it's incremental/
relative (spec does NOT guarantee it either way)
RETRY SAFETY = idempotency. Non-idempotent op (POST, incrementing PATCH)
needs EITHER an absolute-value redesign OR an idempotency key the
server deduplicates against — never blindly retry otherwise.
Status codes: use them HONESTLY — never 200 with an error body.
2xx success / 4xx client error / 5xx server error.
"Pragmatic REST" (HTTP+JSON, sensible resource URLs) ≠ textbook REST
(strict statelessness, full HATEOAS) — most real APIs are the former,
and that's a legitimate, normal choice, not a failure.
Deep resource nesting (4-5+ levels) often signals a flatter,
directly-queryable resource would serve clients better.
The beginner framing: REST is a style for designing web APIs where you model your data as resources (nouns — users, orders, products), and use HTTP's own methods (GET, POST, PUT, DELETE) to describe what you're doing to that resource, rather than inventing a new verb-shaped endpoint for every action.
The precise mental model: REST (REpresentational State Transfer) is an architectural style defined by a specific set of constraints — client-server separation, statelessness (every request must contain everything needed to understand it; the server holds no per-client session state between requests), a uniform interface (resources identified by URIs, manipulated through representations, using standard HTTP methods with well-defined semantics), and cacheability (responses explicitly marked as cacheable or not). Most APIs called "RESTful" in the industry satisfy the uniform-interface and resource-modeling ideas well, but are honestly not stateless in the strict sense (session cookies exist) and rarely implement HATEOAS (Hypermedia as the Engine of Application State — responses containing links to related actions) at all — which is fine; "pragmatic REST" (HTTP + JSON + sensible resource URLs) is what the overwhelming majority of real production APIs actually are, and calling that out honestly is more useful than pretending every API is textbook-pure.
GET /users → list usersPOST /users → create a userGET /users/42 → fetch user 42PUT /users/42 → replace user 42 entirelyPATCH /users/42 → partially update user 42DELETE /users/42 → delete user 42GET /users/42/orders → orders BELONGING TO user 42 (nesting = relationship)
The resource is the noun (users); the HTTP method is the verb. This is the single biggest practical shift from an RPC-style API (POST /getUser, POST /deleteUser) — the URL identifies what, the method identifies the action, and nesting expresses ownership/relationship between resources.
HTTP method semantics — safety and idempotency, precisely#
Method Safe? Idempotent? MeaningGET YES YES read-only, no side effectsHEAD YES YES like GET, headers onlyOPTIONS YES YES discover allowed methodsPUT no YES full replace — calling it N times = same end state as onceDELETE no YES calling it N times = still deleted (2nd call: 404, but state is identical)POST no NO creates a new resource — calling it twice typically creates TWOPATCH no NOT GUARANTEED partial update — idempotent ONLY if you design it that way
Safe means the method causes no server-side state change at all (safe to prefetch, cache, retry blindly). Idempotent means calling it once has the same end-state-on-the-server effect as calling it N times — this is what makes a method safe to automatically retry after a network timeout without fear of duplicating an action. PUT is idempotent because it replaces a resource with a full representation — sending the same replacement twice yields the same end state. POST is explicitly NOT idempotent, because its defining use case is creating something — calling it twice conventionally creates two somethings.
PATCH's idempotency is the detail most engineers get wrong: the HTTP spec does not guarantee PATCH is idempotent — it depends entirely on how the partial update is expressed. PATCH { "views": views + 1 } (an increment) is NOT idempotent — retrying it after a timeout, unsure if the first attempt succeeded, could double-increment. PATCH { "status": "shipped" } (a field set to an absolute value) IS idempotent — retrying it safely converges to the same state. The method name alone doesn't tell you which one you're looking at; the semantics of the patch document do.
// A client's request to increment a product's view count times out —// the client has no way to know if the server actually processed it or not.async function incrementViews(productId) { await fetch(`/products/${productId}`, { method: "PATCH", body: JSON.stringify({ views: { $increment: 1 } }), });}// The client's retry logic, on timeout, just calls it again:try { await incrementViews(
Is it safe for the client to blindly retry this specific PATCH request on timeout?
Solution
No — this is exactly the non-idempotent PATCH trap. If the FIRST request actually succeeded server-side but the response was lost in transit (causing the client-side timeout), the retry sends a SECOND increment — the view count now reflects two increments for one real view. Blind retry is only safe for idempotent operations. The fix is either: make the operation idempotent by design (send an absolute value computed client-side, or use a unique idempotency key the server deduplicates against — see the webhooks/idempotency topic for the general mechanism), or accept that a genuine retry requires first checking current state before deciding whether to resend. This is precisely why the idempotency table above isn't academic trivia — it directly determines which requests are safe to retry automatically versus which need more careful handling.
Build a minimal resource router that demonstrates REST's method+path dispatch — the actual mechanism a framework's routing layer implements:
class ResourceRouter { constructor() { this.routes = []; // { method, pattern, handler } } register(method, pattern, handler) { // convert "/users/:id" into a regex capturing the :id segment const regexPattern = pattern.replace(/:(\w+)/g, "(?<$1>[^/]+)"); this.routes.
This is the essential mechanism every REST framework (Express, Next.js route handlers, Rails) builds on: match the incoming method AND path pattern together, extract path parameters, dispatch to the matching handler. The resource-oriented URL design directly enables this simple, uniform dispatch — the alternative (RPC-style POST /doThing) pushes all the "what action" logic into the request body instead of the URL, losing this clean method+path routing structure.
REST's method semantics build directly on the HTTP protocol fundamentals covered in HTTP & Networking — safety and idempotency aren't REST-specific inventions, they're properties defined by the HTTP specification itself that REST design leans on heavily. Status-code discipline and structured error responses (RFC 9457 application/problem+json) — the natural next layer once resource/method modeling is settled — are covered in API Design & Versioning. And a GET request's safety/idempotency is exactly what makes it eligible for the caching mechanisms covered in API Caching — a mutating method fundamentally can't be cached the same way.
POST /createUser // ❌ RPC-style, redundant with the POST method itselfPOST /users // ✅ the method already says "create"
If the URL contains a verb that duplicates what the HTTP method already expresses, the resource model is inverted — the method IS the verb; the URL should be the noun.
As shown in Try It — PATCH's idempotency depends entirely on whether the patch document expresses an absolute value or a relative/incremental change. Never assume PATCH is safe to retry without checking which kind it is.
res.status(200).json({ error: "User not found" }); // ❌ status code lies about what happened
This defeats the entire point of HTTP status codes as a machine-readable outcome signal — clients, proxies, caches, and monitoring tools all key off the status code, not the body content, to determine success/failure. A "successful" 200 response body containing an error string breaks all of that tooling.
Model URLs around resources (nouns), use HTTP methods for actions — GET/POST/PUT/PATCH/DELETE /resource(/:id), nesting to express ownership (/users/:id/orders).
Know precisely which methods are safe and which are idempotent — this directly determines what's safe to retry, cache, or prefetch automatically.
Never assume PATCH is idempotent without checking the specific patch semantics — design it to be (absolute values, not increments) whenever retry-safety matters.
Use status codes honestly — 2xx for success, 4xx for client error, 5xx for server error, never 200 with an error payload.
Be honest about how "RESTful" an API actually is — most production APIs are pragmatic HTTP+JSON, not textbook HATEOAS-compliant REST, and that's a legitimate, common design choice, not a failure to hit some purity bar.
Safe, idempotent methods (GET) are what make HTTP caching, prefetching, and CDN edge-caching possible at all — mutating methods can't participate in the same caching layer, which is a direct performance reason to keep reads and writes on the methods that actually match their semantics.
Resource nesting depth has a real cost: /users/:id/orders/:orderId/items/:itemId/reviews is technically valid but often signals the API should expose a flatter, directly-queryable resource (/reviews?itemId=...) instead — deep nesting forces clients into multiple round-trips to reach data that could often be fetched directly.
42
);
} catch {
await incrementViews(42); // retry
}
push
({ method, regex:
new
RegExp
(
`^${
regexPattern
}$`
), handler });
}
dispatch(method, path) {
for (const route of this.routes) {
if (route.method !== method) continue;
const match = path.match(route.regex);
if (match) return route.handler(match.groups ?? {});