API PlaybookAPI Design
API DesignBeginner6 min

REST Design Principles

Resources and state transfers, not database table wrappers

In a nutshell

REST is the architectural style behind most web APIs. The core idea is simple: everything is a resource (like a user or an order) with its own URL, and you use standard HTTP methods -- GET to read, POST to create, PUT to replace, PATCH to update, DELETE to remove. Each request is self-contained, meaning the server doesn't remember anything between calls. Understanding these fundamentals helps you build APIs that work naturally with the entire HTTP ecosystem.

The situation

A new engineer joins your team and asks why GET /users returns a list but GET /users/123/activate changes state. Someone else asks why the API uses POST for everything. A third person brings up HATEOAS in a code review and nobody knows whether to take it seriously.

REST is the most used and least understood architectural style in software. Most "REST APIs" are really "JSON over HTTP" — which is fine, as long as you understand what you're actually building.

REST in 60 seconds

REST (Representational State Transfer) is an architectural style with a core idea: clients interact with resources by exchanging representations of their state.

In practice, that means:

  1. Everything is a resource — identified by a URI (/orders/456)
  2. Interactions use standard HTTP methods — GET, POST, PUT, PATCH, DELETE
  3. Representations carry the state — typically JSON bodies
  4. The server is stateless — each request contains everything needed to process it

That's it. Is it "REST" or "RESTful"? Honestly, this debate isn't worth your time. If you're using resources, standard HTTP methods, and JSON representations, you're close enough. Spend your energy on good API design, not on theological compliance with Roy Fielding's thesis.

HTTP methods: what each one means

GET — Read a resource

GET /api/orders/456
HTTP/1.1 200 OK

{
  "id": "order_456",
  "customer": "cust_123",
  "items": [
    { "productId": "prod_A", "quantity": 2, "unitPrice": 29.99 }
  ],
  "total": 59.98,
  "status": "confirmed",
  "createdAt": "2026-04-10T14:22:00Z"
}

GET is safe (no side effects) and idempotent (calling it 10 times produces the same result). Never use GET to modify state.

POST — Create a resource

POST /api/orders
Content-Type: application/json

{
  "customer": "cust_123",
  "items": [
    { "productId": "prod_A", "quantity": 2 }
  ]
}
HTTP/1.1 201 Created
Location: /api/orders/order_789

{
  "id": "order_789",
  "customer": "cust_123",
  "items": [
    { "productId": "prod_A", "quantity": 2, "unitPrice": 29.99 }
  ],
  "total": 59.98,
  "status": "pending",
  "createdAt": "2026-04-13T09:15:00Z"
}

POST is not idempotent — sending the same request twice creates two orders. Return 201 Created with the full resource and a Location header.

DELETE — Remove a resource

DELETE /api/orders/order_789
HTTP/1.1 204 No Content

DELETE is idempotent — deleting the same resource twice should not fail. The first call deletes it, the second returns 204 (or 404, both are acceptable patterns).

PUT vs PATCH: the difference that matters

This is the most commonly misunderstood part of REST. Both update a resource, but they do it differently.

PUT — Full replacement

PUT replaces the entire resource with the provided representation. If you omit a field, it gets removed (or reset to its default).

PUT /api/orders/order_456
Content-Type: application/json

{
  "customer": "cust_123",
  "items": [
    { "productId": "prod_A", "quantity": 3 },
    { "productId": "prod_B", "quantity": 1 }
  ],
  "status": "confirmed"
}
HTTP/1.1 200 OK

{
  "id": "order_456",
  "customer": "cust_123",
  "items": [
    { "productId": "prod_A", "quantity": 3, "unitPrice": 29.99 },
    { "productId": "prod_B", "quantity": 1, "unitPrice": 14.99 }
  ],
  "total": 104.96,
  "status": "confirmed",
  "createdAt": "2026-04-10T14:22:00Z"
}

PUT is idempotent — sending the same PUT request 5 times produces the same result.

PATCH — Partial update

PATCH updates only the fields you send. Everything else stays unchanged.

PATCH /api/orders/order_456
Content-Type: application/json

{
  "status": "shipped"
}
HTTP/1.1 200 OK

{
  "id": "order_456",
  "customer": "cust_123",
  "items": [
    { "productId": "prod_A", "quantity": 3, "unitPrice": 29.99 },
    { "productId": "prod_B", "quantity": 1, "unitPrice": 14.99 }
  ],
  "total": 104.96,
  "status": "shipped",
  "createdAt": "2026-04-10T14:22:00Z"
}

Only status changed. The items, customer, and total remain untouched.

The practical choice

Most real-world APIs use PATCH for updates because clients rarely want to send the entire resource back. PUT is useful when you genuinely want to replace the whole thing — like overwriting a configuration file or replacing a document. When in doubt, support PATCH.

Side-by-side comparison

AspectPUTPATCH
SemanticsFull replacementPartial update
Missing fieldsReset to default or removedLeft unchanged
IdempotentYesTechnically no (depends on implementation)
Request bodyComplete resource representationOnly changed fields
Common use caseReplace a config, overwrite a docUpdate a user's email, change an order status

What stateless really means

Every request must contain all the information the server needs to process it. The server doesn't store client session state between requests.

# Stateful (server remembers who you are from a previous call)
GET /api/my-orders
# Server: "Who is 'my'? Oh right, I have a session for you."

# Stateless (every request identifies itself)
GET /api/orders?customer=cust_123
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
# Server: "You're cust_123, authenticated by this token. Here are your orders."

Statelessness is what makes REST APIs horizontally scalable. Any server in the cluster can handle any request — no sticky sessions, no shared memory.

Sessions are not stateless

If your API requires a prior call to "initialize" something before subsequent calls work, you've introduced hidden state. Each request should stand on its own.

Content negotiation

The Content-Type header declares what format the request body is in. The Accept header declares what format the client wants in the response.

POST /api/orders
Content-Type: application/json
Accept: application/json

{ "customer": "cust_123" }

In practice, nearly everything is JSON. But the mechanism exists for APIs that support XML, Protocol Buffers, or other formats. It becomes more relevant when you get into API versioning via content negotiation (covered in the schema evolution topic).

Quick method reference

MethodPurposeIdempotentSafeRequest body
GETReadYesYesNo
POSTCreateNoNoYes
PUTFull replaceYesNoYes
PATCHPartial updateNo*NoYes
DELETERemoveYesNoRarely
HEADRead headers onlyYesYesNo
OPTIONSDiscover capabilitiesYesYesNo

*PATCH can be implemented as idempotent, but the spec doesn't guarantee it.

Method safety matters for caching

Browsers and CDNs cache GET and HEAD responses automatically. They never cache POST, PUT, PATCH, or DELETE. Using methods correctly means free caching behavior.


Next up: query design — filtering, sorting, pagination, and sparse fieldsets for when a simple GET isn't enough.