DeepFrontend
Practice ArenaBlogSign in
Go Pro
DeepFrontend

Interview-grade learning paths and hands-on practice for mid-to-senior engineers. Master internals, patterns, and system architecture with runnable code.

All systems operational

Core Courses

  • -JavaScript Internals
  • -React Reconciliation
  • -TypeScript rigor
  • -Next.js Caching
  • -Node.js Event Loop
  • -System Design
  • -Web Security
  • -CSS & Page Layouts

Explore

  • Learning Paths
  • Practice Arena
  • Pricing Options
  • HTML Sitemap

Resources

  • Privacy Policy
  • Terms of Service
  • Email Support

© 2026 DeepFrontend. All rights reserved.

Expert learning environments for web engineering teams.

Home/API Development/REST Design & Best Practices
intermediate25 min read·Updated Jul 2026

REST Design & Best Practices

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.

Related topics

HTTP & Networking (HTTP/1.1 vs 2 vs 3)ProAPI Design & VersioningProAPI CachingPro

On this page

25m
Knowledge CheckInterview QuestionsCheatsheet

Concept#

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 users
POST   /users           → create a user
GET    /users/42        → fetch user 42
PUT    /users/42         → replace user 42 entirely
PATCH  /users/42         → partially update user 42
DELETE /users/42         → delete user 42
 
GET    /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?   Meaning
GET      YES    YES           read-only, no side effects
HEAD     YES    YES           like GET, headers only
OPTIONS  YES    YES           discover allowed methods
PUT      no     YES           full replace — calling it N times = same end state as once
DELETE   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 TWO
PATCH    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.


Try It#

Predict the outcome before checking the solution.

// 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.


Implement It Yourself#

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.


Under the Hood#

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.


Common Mistakes#

1. Verbs in the URL instead of resource nouns#

POST /createUser        // ❌ RPC-style, redundant with the POST method itself
POST /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.

2. Treating PATCH as automatically safe to retry#

// PATCH { balance: { $increment: -50 } } // ❌ non-idempotent, unsafe to blindly retry

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.

3. Using 200 OK for everything, including errors#

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.


Best Practices#

  • 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.

Performance Tips#

  • 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 ?? {});
}
return { status: 404, body: "Not Found" };
}
}
const router = new ResourceRouter();
router.register("GET", "/users/:id", (params) => ({ status: 200, body: `user ${params.id}` }));
router.register("GET", "/users/:id/orders", (params) => ({ status: 200, body: `orders for user ${params.id}` }));
router.register("DELETE", "/users/:id", (params) => ({ status: 204, body: null }));
console.log(router.dispatch("GET", "/users/42")); // { status: 200, body: 'user 42' }
console.log(router.dispatch("GET", "/users/42/orders")); // { status: 200, body: 'orders for user 42' }
console.log(router.dispatch("DELETE", "/users/42")); // { status: 204, body: null }
console.log(router.dispatch("POST", "/users/42")); // { status: 404, body: 'Not Found' } — no POST handler registered for this exact path