REST Principles: Resources, URLs, Methods, Statelessness & Idempotency
Understand what makes an HTTP API 'RESTful': modeling data as resources with predictable URLs, using HTTP methods for their intended semantics, returning representations, staying stateless, and knowing which operations must be idempotent.
REST (Representational State Transfer) isn't a protocol or a library — it's a set of conventions for designing HTTP APIs so that URLs, methods, and status codes carry meaning a client can rely on without reading custom documentation for every endpoint. A well-designed REST API feels predictable: once you know /orders/123 returns an order, you can guess that DELETE /orders/123 deletes it and GET /orders/123/items lists its line items, without anyone telling you. Badly designed APIs — action-named endpoints like /getOrderItems?id=123&action=list — throw that predictability away and force every client to memorize a bespoke contract per endpoint. This lesson covers the five ideas that make an API RESTful: resources as nouns, URL design that reflects relationships, correct use of HTTP methods, JSON representations of state, statelessness, and idempotency — the property that lets clients safely retry failed requests.
Resources are nouns, not actions
The core idea of REST is that everything your API exposes is a *resource* — a noun, addressable by a URL — and you operate on it with a small, fixed set of HTTP verbs. An 'order' is a resource. A 'customer' is a resource. 'Get the order items' is not a resource, it's an action, and REST asks you to reframe it as a resource too: the order's items, at /orders/123/items. Once you think this way, most CRUD-shaped features fall out of the URL structure automatically, and you stop inventing endpoint names like /createOrder or /cancelOrder — those become POST /orders and POST /orders/123/cancel (or better, PATCH /orders/123 with {"status":"cancelled"} when it's really just a state change).
Collections and single items get distinct, consistent URL shapes: a collection URL like /orders represents 'all orders', and appending an identifier — /orders/123 — narrows it to one specific order. Nesting expresses ownership: /orders/123/items is 'the items that belong to order 123', which is both self-documenting and lets you reuse the same pattern (/customers/456/orders) everywhere in the API without a design meeting.
• Good: /orders, /orders/123, /orders/123/items, /orders/123/items/9. • Bad: /getOrder?id=123, /getOrderItems?id=123, /order-cancel.php?id=123. • Use plural nouns for collections (/orders, not /order) — it reads naturally for both the collection and a single member. • Actions that don't map cleanly to CRUD (e.g. 'cancel', 'refund') become sub-resources or state transitions: POST /orders/123/cancel, not a new verb tacked onto the query string.
HTTP methods carry semantics
REST reuses the HTTP methods you already know instead of inventing new verbs, and each one has a specific, well-understood meaning that clients, proxies, caches, and browsers all agree on. GET retrieves a representation and must never change server state — a monitoring tool or browser prefetcher can call GET freely without side effects. POST creates a new resource inside a collection (POST /orders creates a new order and typically returns 201 Created with a Location header pointing at /orders/124). PUT replaces a resource wholesale at a known URL. PATCH applies a partial update. DELETE removes a resource.
Picking the wrong verb breaks assumptions built into the web itself. If GET /orders/123/delete actually deletes the order, a link-prefetching browser or a web crawler following that link will silently destroy data — this is a real, well-documented class of bug from APIs that ignored HTTP semantics. Sticking to the verb table below keeps your API compatible with caches, browsers, and every HTTP client library without special-casing.
• GET /orders — list orders (safe, cacheable, no side effects). • GET /orders/123 — fetch one order. • POST /orders — create a new order (server assigns the id). • PUT /orders/123 — replace order 123 entirely with the given representation. • PATCH /orders/123 — partially update order 123 (only the given fields). • DELETE /orders/123 — remove order 123.
Representations: JSON in, JSON out
A resource is a concept — 'order 123' — and a *representation* is a concrete serialization of its current state that the client and server exchange, most commonly JSON today (though the same resource could also be represented as XML or CSV — REST doesn't mandate JSON, it just needs to be negotiable and self-describing via the Content-Type header). The server sets Content-Type: application/json on responses, and a well-behaved client sends Accept: application/json and Content-Type: application/json on requests carrying a JSON body.
Representations should be stable and predictable in shape: the same resource type returns the same field names and types every time, null is used consistently for 'absent', and nested resources are either embedded or linked by id/URL — not sometimes one, sometimes the other. This predictability is what lets you generate client SDKs and API documentation automatically from real responses.
Statelessness: every request stands alone
In a stateless API, the server keeps no memory of previous requests from a client between calls — every request must carry everything needed to understand and authorize it (an auth token in the Authorization header, all the parameters it needs, etc.). This is different from a traditional server-rendered web app that stores 'logged in as user 42' in a $_SESSION on the server between page loads. A REST API instead validates a bearer token or API key on every single request.
Statelessness is what makes REST APIs horizontally scalable: because no server instance holds client-specific state in memory, any request can be routed to any server behind a load balancer, and a server can be restarted or replaced without losing anyone's 'session'. It also makes APIs easier to reason about and test — you can replay any single request in isolation and get the same result, because nothing outside that request (aside from the resource's own state in the database) affects the outcome.
Idempotency: safe to retry
An operation is *idempotent* if performing it multiple times has the same effect as performing it once. This matters enormously for real networks, where a client can time out waiting for a response even though the server actually processed the request — the client doesn't know whether to retry. If the operation is idempotent, retrying is always safe. GET, PUT, and DELETE are specified to be idempotent: fetching a resource repeatedly doesn't change it; replacing a resource with the same payload twice leaves it in the same final state; deleting an already-deleted resource is still 'deleted' (a well-behaved API returns 204 or 404 either way, not an error that implies something went wrong).
POST is deliberately *not* idempotent by default — calling POST /orders twice with the same body creates two separate orders, because POST means 'create a new thing in this collection' and 'a new thing' happens every time. When a POST-like operation genuinely needs retry-safety (e.g. charging a payment), APIs add an explicit Idempotency-Key request header: the server remembers which keys it has already processed and returns the original result instead of creating a duplicate on a retried request with the same key.
• Idempotent by spec: GET, HEAD, PUT, DELETE, OPTIONS. • Not idempotent: POST (creates a new resource each call, unless you add an idempotency key). • PATCH is technically not guaranteed idempotent (e.g. 'increment counter by 1' patches aren't), though most field-replacement PATCHes in practice are.
Resource-oriented URL patterns
Nesting expresses ownership: items only make sense in the context of an order. IDs go in the path, not the query string — /orders/123, never /orders?id=123 for a single resource. Reserve query parameters for filtering, sorting, pagination, and field selection on a collection, not for identifying a specific resource.
GET /orders # list orders (paginated)
POST /orders # create an order -> 201 + Location
GET /orders/{id} # fetch one order
PUT /orders/{id} # replace order {id}
PATCH /orders/{id} # partially update order {id}
DELETE /orders/{id} # delete order {id} -> 204
GET /orders/{id}/items # list items on order {id}
POST /orders/{id}/items # add an item to order {id}
GET /orders/{id}/items/{iid}
DELETE /orders/{id}/items/{iid}Code Examples
The resource-oriented router parses the URL path into resource segments and dispatches on HTTP method, so /orders/123 naturally supports GET/PUT/PATCH/DELETE and /orders/123/items naturally supports GET/POST — no per-action endpoint names needed. A method that isn't supported on that resource returns 405 Method Not Allowed with an Allow header, which is itself a REST convention clients can rely on.
match ($method) { 'GET' => sendOrder($orderId),... } Common mistake: Naming endpoints after actions (/getOrderItems.php) instead of resources means every new operation requires inventing and documenting a brand-new endpoint name, and nothing about the URL tells a client whether it's safe to retry or cache.
DELETE and PUT are written so that calling them once or twice in a row leaves the system in the same state, matching their idempotent contract. Because POST is not idempotent by spec, retry-safety for order creation is added explicitly via a client-supplied Idempotency-Key header, with the server storing and replaying the first response for a repeated key instead of creating a duplicate order. Expected result: 204 No Content (DELETE) · 200 OK (PUT) · 201 Created, then 200 on a retried POST with the same key
<?php
declare(strict_types=1);
// --- BAD: action-style, RPC-flavored endpoints ---
// GET /getOrderItems.php?id=123
// POST /cancelOrder.php?id=123
// Every endpoint needs its own bespoke name and the client has to
// memorize which ones are safe to prefetch/cache and which aren't.
// --- GOOD: resource-oriented, verbs carry meaning ---
$method = $_SERVER['REQUEST_METHOD'];
$path = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
$parts = explode('/', $path); // e.g. ["orders", "123", "items"]
if ($parts[0] === 'orders' && isset($parts[1]) && !isset($parts[2])) {
$orderId = (int) $parts[1];
match ($method) {
'GET' => sendOrder($orderId), // fetch representation
'PUT' => replaceOrder($orderId), // idempotent full replace
'PATCH' => updateOrder($orderId), // partial update
'DELETE' => deleteOrder($orderId), // idempotent delete
default => methodNotAllowed(['GET', 'PUT', 'PATCH', 'DELETE']),
};
} elseif ($parts[0] === 'orders' && isset($parts[1]) && $parts[2] === 'items') {
$orderId = (int) $parts[1];
match ($method) {
'GET' => sendOrderItems($orderId),
…<?php
declare(strict_types=1);
// DELETE is idempotent: deleting something already gone is still "gone".
function deleteOrder(PDO $pdo, int $id): void {
$stmt = $pdo->prepare('DELETE FROM orders WHERE id = ?');
$stmt->execute([$id]);
// Whether a row was actually deleted or not, the END STATE the client
// asked for ("order 123 does not exist") now holds either way.
// A retried DELETE after a lost response still returns 204, not an error.
http_response_code(204);
}
// PUT is idempotent: sending the same full representation twice
// leaves the resource in the same final state both times.
function replaceOrder(PDO $pdo, int $id, array $body): void {
$stmt = $pdo->prepare(
'UPDATE orders SET status = ?, total_cents = ?, notes = ? WHERE id = ?'
);
$stmt->execute([$body['status'], $body['total_cents'], $body['notes'] ?? null, $id]);
http_response_code(200);
}
// POST is NOT idempotent by default: two identical calls create two orders.
// To make a "create" retry-safe, accept a client-supplied Idempotency-Key.
function createOrder(PDO $pdo, array $body, ?string $idempotencyKey): array {
if ($idempotencyKey !== null) {
$exis
…Resource hierarchy vs. action-style endpoints
Nesting resources under their owner mirrors the data relationship; action names don't scale.
RESOURCE-ORIENTED (REST) ACTION-ORIENTED (RPC-style, avoid)
───────────────────────── ───────────────────────────────────
/orders /getOrders.php
GET -> list /createOrder.php?...
POST -> create /getOrderById.php?id=123
/orders/{id} /updateOrder.php?id=123
GET -> fetch /deleteOrder.php?id=123
PUT -> replace /cancelOrder.php?id=123
PATCH -> partial update /getOrderItems.php?id=123
DELETE-> remove /addItemToOrder.php?id=123
/orders/{id}/items
GET -> list items Every new feature = a new endpoint
POST -> add item name to invent, document, and
/orders/{id}/items/{iid} teach every client about by hand.
GET/DELETE -> one itemImportant Rules
Model your domain as nouns (resources) addressable by URL, not verbs baked into the endpoint name.
Use GET for reads only — never trigger a state change from a GET, since it must remain safe to prefetch and cache.
PUT and DELETE must be idempotent: calling them N times has the same effect as calling them once.
POST creates and is not idempotent by default — add an Idempotency-Key header when retry-safety is required.
Keep representations (JSON field names/types) consistent for a given resource across every endpoint that returns it.
Never store per-client state on the server between requests — every request must carry its own full context (auth, parameters).
Why this matters at scale
Stripe, GitHub, and Shopify's public APIs are textbook examples of resource-oriented design: /v1/charges/{id}, /repos/{owner}/{repo}/issues/{number}, /admin/api/orders/{id}.json. Stripe in particular popularized the Idempotency-Key header pattern specifically because payment APIs cannot tolerate double-charges from network retries — it's now copied by nearly every serious payments and infrastructure API.
GET /deleteOrder.php?id=123 DELETE /orders/123 A GET that mutates state breaks the HTTP contract browsers, crawlers, and caches rely on — a prefetching browser or link-checking bot could delete data just by following a link. Mutations belong behind POST/PUT/PATCH/DELETE.
Treating POST /orders as safe to blindly retry on a timeout Attach an Idempotency-Key header to POST /orders and have the server dedupe on it
POST is not idempotent — retrying a timed-out create without an idempotency key risks creating duplicate orders (and duplicate charges, in a payments API). The server must explicitly opt POST into retry-safety.
Which HTTP method is NOT idempotent by default? A. GET B. PUT C. DELETE D. POST
D. POST — POST creates a new resource on every call, so calling it twice with the same body produces two resources — it is not idempotent unless the API explicitly adds retry-safety (e.g. an Idempotency-Key).
Which URL best follows REST resource-oriented design for fetching the items in order 123? A. /getOrderItems.php?id=123 B. /orders/123/items C. /items?orderId=123&action=list D. /order-items-list?order=123
B. /orders/123/items — /orders/123/items expresses the ownership relationship directly in the path and reuses the same GET-for-read convention as every other collection endpoint, without inventing a new action name.
What does 'statelessness' mean in a REST API? A. The server never uses a database B. Every request must carry all the context needed to process it; the server keeps no per-client session between requests C. The API has no error states D. Resources never change over time
B. Every request must carry all the context needed to process it; the server keeps no per-client session between requests — Statelessness means the server does not remember anything about a specific client between requests — auth, parameters, and context must all travel with each request, which is what makes REST APIs trivially horizontally scalable.
Design resource-oriented URLs (with HTTP methods) for a blogging API that needs to: list a blog's posts, create a post, fetch/update/delete a single post, list a post's comments, and add a comment. Then identify which of these operations must be idempotent.
GET /blogs/{id}/posts, POST /blogs/{id}/posts, GET/PUT/DELETE /posts/{id}, GET /posts/{id}/comments, POST /posts/{id}/comments. GET and DELETE on /posts/{id} must be idempotent; PUT /posts/{id} must be idempotent (same replacement body -> same end state); POST to create a post or comment is not idempotent by default.
Start from the resource hierarchy: a blog owns posts, a post owns comments. Nest accordingly, and remember only GET/PUT/DELETE are idempotent by default.