12 min read

Chapter 2: REST Constraints — Why REST Works

"REST is not a standard. It is an architectural style —
a set of constraints that, when followed, produce a system with desirable properties." — Roy Fielding, PhD Dissertation, 2000


1. Physical Reality First

In 2000, Roy Fielding — one of the authors of the HTTP specification — finished his PhD thesis at UC Irvine. He wasn't inventing something new. He was looking at the web — the most successful distributed system ever built — and asking: why does it work so well?

The web at that point had:

  • Billions of documents
  • Thousands of different server implementations
  • Hundreds of different client implementations (browsers)
  • No central coordination
  • Near-zero coupling between clients and servers

A browser written in 1996 could still load a web page from a server written in 2005. How? Because both followed the same constraints. Fielding named those constraints REST: REpresentational State Transfer.

REST is not a protocol. It is not a library. It is not a spec you implement.
It is a set of 6 architectural constraints that, when applied to HTTP, produce APIs with the same properties as the web: scalable, decoupled, long-lived.

The reason most "REST APIs" are actually not RESTful: engineers copy the URL style but ignore the constraints.


2. The Six REST Constraints

Constraint 1: Client-Server

The rule: Client and server are separate. Client handles UI/UX. Server handles data and business logic. They communicate only through a defined interface.

Why this matters: Decoupling. You can:

  • Rewrite your React frontend without touching the API
  • Rewrite your Postgres backend without touching the mobile app
  • Scale them independently

What violates it: Server-side rendering where the server generates HTML that contains embedded application state. Or a backend that directly imports frontend code.

In practice: Your mobile app, web app, and third-party integrations all hit the same API. The server doesn't know or care what the client looks like. This is the constraint most APIs actually follow correctly.


Constraint 2: Statelessness

The rule: Every request must contain all information needed to process it. The server must not store any session state between requests.

This is the most important and most misunderstood constraint.

What it means physically:

code
❌ Stateful (violates REST):
  Request 1: POST /login         → Server saves session: {user: 42, logged_in: true}
  Request 2: GET /orders         → Server looks up session to find user 42

✅ Stateless (REST):
  Request 1: POST /auth/token    → Server returns JWT token
  Request 2: GET /orders         → Client sends JWT. Server reads user from token.
                                    No session lookup needed.

In the stateful model, the server must remember something between requests. This creates coupling between the client and a specific server instance. If you have 10 servers, request 2 must go to the same server as request 1 — or you need sticky sessions, or a shared session store.

In the stateless model, any server can handle any request. Load balancers can route freely. Servers can crash and restart without losing "session". Horizontal scaling becomes trivial.

What statelessness does NOT mean: The server can't have a database. Data persisted in a database is resource state, not session state. The distinction:

TypeExampleAllowed?
Session state"This user is logged in on server 3"❌ No
Resource stateUser record in Postgres✅ Yes
Application stateCurrent state of a workflow✅ Yes (in DB)

Cost of statelessness: Every request must carry credentials (token, API key). Requests are slightly larger. There's no "remember where I was" on the server — the client must track this.


Constraint 3: Cacheability

The rule: Responses must declare themselves as cacheable or non-cacheable. Clients and intermediaries (CDNs, proxies) may cache cacheable responses.

Why this matters: Network is expensive. If a response can be reused, it should be. A CDN caching your GET /products response for 60 seconds eliminates 99% of those requests from ever reaching your server.

How HTTP implements this:

code
# Server says: cache this for 1 hour
Cache-Control: max-age=3600, public

# Server says: never cache this
Cache-Control: no-store

# Server says: you can cache, but check with me before using
Cache-Control: no-cache

# Server says: cache, but only in the browser (not CDN)
Cache-Control: private, max-age=600

What this connects to: Chapter 8 covers HTTP caching in depth — ETags, conditional requests, cache invalidation. The constraint here is simply: your API must think about caching. Most APIs don't, and they pay for it in load.

What violates it: Returning caching headers that don't reflect reality. E.g., caching a response that contains user-specific data as public. Now every user gets each other's data from the CDN.


Constraint 4: Uniform Interface

This is the defining constraint of REST. It's what makes REST different from RPC.

It has four sub-constraints:

4a. Resource Identification in Requests
Resources are identified by URIs. The URI identifies what the resource is, not what to do with it.

code
✅ REST:    GET /users/42
❌ RPC:     POST /getUser  {"id": 42}

4b. Resource Manipulation Through Representations
Clients interact with resources through representations (JSON, XML). The representation contains enough information to modify or delete the resource.

You GET a user → receive JSON → modify the JSON → PUT it back. The representation is the contract.

4c. Self-Descriptive Messages
Each message contains enough information to describe how to process it. Content-Type: application/json tells the receiver how to parse the body. The method tells the receiver what action to take. No out-of-band knowledge needed.

4d. HATEOAS — Hypermedia as the Engine of Application State
This is the most ignored sub-constraint. Responses include links to related actions. Clients discover API capabilities through these links rather than hardcoding URLs.

json
// Response to GET /orders/99
{
  "id": 99,
  "status": "pending",
  "total": 2500,
  "_links": {
    "self":    { "href": "/orders/99" },
    "cancel":  { "href": "/orders/99/cancel", "method": "POST" },
    "payment": { "href": "/orders/99/payment" },
    "user":    { "href": "/users/42" }
  }
}

The client doesn't need to know the URL for canceling an order. The response tells it. If the order is already shipped, the cancel link won't appear — no need for the client to know cancellation rules.

Why HATEOAS is almost never implemented in practice: It requires significant server-side work, clients need to be built to follow links dynamically, and most API clients are written by the same team as the server. The coupling HATEOAS prevents is often not a real concern in practice.

The honest truth: Most "REST APIs" follow constraints 1, 2, 3, and partially 4 (they use URIs and HTTP methods but skip HATEOAS). That is still enormously better than raw RPC. You don't fail an interview for not implementing HATEOAS — but you should know it exists and why.


Constraint 5: Layered System

The rule: The client should not be able to tell whether it's connected directly to the server or to an intermediary. You can add layers (load balancers, CDNs, API gateways, caches) without the client knowing.

code
Client → CDN → API Gateway → Load Balancer → App Server → DB

The client thinks it's talking to api.example.com. It doesn't know (or care) about the 4 layers in between.

Why this matters: You can:

  • Add a CDN layer without changing client code
  • Add an API gateway for auth/rate limiting without changing app code
  • Add a caching proxy without changing either
  • Swap the entire backend without the client noticing

What violates it: An API that exposes internal infrastructure details. E.g., returning X-Served-By: db-replica-3.internal.example.com in a response — the client now knows your internal topology and might try to use that knowledge.


Constraint 6: Code on Demand (Optional)

The rule: Servers can extend client functionality by sending executable code (JavaScript in browsers).

Why it's optional: Doesn't apply to most REST APIs. The web uses it (browsers execute JS from servers). Mobile APIs don't — you can't send code to a native app.

Ignore this for backend API design. It's listed for completeness.


3. The Richardson Maturity Model

Leonard Richardson (2008) gave us a rubric for measuring how RESTful an API actually is. Four levels:

code
Level 0 — The Swamp of POX (Plain Old XML)
Level 1 — Resources
Level 2 — HTTP Verbs
Level 3 — Hypermedia (HATEOAS)

Level 0: One endpoint, one action

code
POST /api
{"action": "getUser", "id": 42}

POST /api
{"action": "createOrder", "items": [...]}

This is XML-RPC or SOAP. HTTP is used as a dumb transport. No resources, no verbs, no semantics. Every call is a POST to the same URL.

Real examples: Old SOAP web services, some GraphQL implementations technically live here.


Level 1: Resources

code
POST /users/getUser       {"id": 42}
POST /orders/createOrder  {"items": [...]}

Different URLs for different resources — good. But still using POST for everything. The URL is doing some semantic work but the method isn't.


Level 2: HTTP Verbs ← Where most "REST APIs" live

code
GET    /users/42          → fetch user
POST   /users             → create user
PUT    /users/42          → replace user
DELETE /users/42          → delete user

Resources + correct HTTP verbs + proper status codes. This is what engineers mean when they say "RESTful API." It's correct, useful, and what you should build.

The gap from Level 1 to Level 2 is about:

  • Using GET for reads (enabling caching)
  • Using DELETE for deletions (semantic clarity)
  • Using 201 for creation (vs. 200 with {"success": true})
  • Using 404 when a resource doesn't exist (vs. 200 with {"error": "not found"})

Level 3: Hypermedia (HATEOAS)

json
GET /users/42 →
{
  "id": 42,
  "name": "Trilochan",
  "_links": {
    "self":    "/users/42",
    "orders":  "/users/42/orders",
    "update":  { "href": "/users/42", "method": "PUT" },
    "delete":  { "href": "/users/42", "method": "DELETE" }
  }
}

The response tells you what you can do next. Clients are navigating the API like a web browser follows links — without hardcoded knowledge of URL structure.

Target for production: Level 2. Know Level 3 exists. Implement it when clients are genuinely decoupled (public APIs, partner integrations).


4. REST vs RPC vs GraphQL — The Honest Comparison

REST (Level 2)RPC (gRPC)GraphQL
Core ideaResources + HTTP verbsFunctions over networkQuery language for graph
URL structure/users/42/ordersUserService.GetOrdersPOST /graphql
CachingBuilt-in (GET is cacheable)Hard (all POST)Hard (all POST)
SchemaOpenAPI (optional)Protobuf (mandatory)Schema (mandatory)
OverfetchingCommon (get whole resource)Not a problemNot a problem
Browser friendlyYesNo (requires gRPC-web)Yes
Best forPublic APIs, web/mobileInternal microservicesComplex client data needs

When to use what:

  • Public API (third parties, mobile, web): REST
  • Internal service-to-service (microservices): gRPC (faster, typed, binary)
  • Frontend needing flexible queries (dashboard, complex UI): GraphQL

These are not competitors. Stripe uses REST for their public API and gRPC internally between services.


5. How Real Systems Apply REST Constraints

Stripe (constraint: statelessness)
Every Stripe API call includes the API key and idempotency key. No session. Any Stripe server can handle any request. This is what allows them to have 99.9999% uptime — servers can fail and restart without losing client state.

GitHub API (constraint: uniform interface + cacheability)
GET /repos/torvalds/linux/commits is a proper resource identifier. The response includes ETag and Cache-Control. GitHub's CDN (Fastly) caches public API responses. GitHub returns Link headers with HATEOAS-style pagination links.

Twilio (constraint: client-server)
Twilio's API is completely decoupled from how you built your app. Ruby, Python, Node, Java — they all hit the same API. The server has no idea what language the client uses.

AWS S3 (constraint: layered system)
When you call PUT /my-bucket/my-object, you're potentially hitting a CloudFront edge, then an API Gateway, then regional S3 infrastructure. You have no visibility into any of this. The client-server interface is clean.


6. Vocabulary Map

TermDefinition
RESTRepresentational State Transfer. An architectural style for distributed hypermedia systems, defined by Roy Fielding in 2000.
ConstraintA rule that an architecture must follow to qualify as RESTful. Violations lose the architectural benefits.
StatelessServer stores no session state between requests. Every request is self-contained.
Session stateServer-side memory of a client's context (who you are, where you are in a workflow). Violates REST.
Resource stateData persisted in a database (user records, orders). Allowed in REST.
Uniform InterfaceThe defining REST constraint: resources identified by URIs, manipulated via standard HTTP methods, self-descriptive messages, HATEOAS.
HATEOASHypermedia As The Engine Of Application State. Responses include links to available next actions.
RepresentationThe specific format of a resource sent in a request/response (JSON, XML). Not the resource itself.
Richardson Maturity Model4-level rubric measuring RESTfulness: Level 0 (RPC), Level 1 (resources), Level 2 (verbs), Level 3 (HATEOAS).
CacheabilityREST constraint requiring responses to declare whether they can be cached.
Layered SystemREST constraint: client cannot tell if it's talking to origin server or intermediary.
RPCRemote Procedure Call. Treat network calls like function calls. gRPC is modern RPC.
GraphQLQuery language for APIs. Client specifies exactly what data it needs.
IdempotentOperation safe to retry: same result whether called once or N times. (Carried forward from Ch1.)

7. Practice Problems

Problem 1 — Failure Analysis:
Your API uses server-side sessions stored in memory. It works perfectly with 1 server. After scaling to 3 servers with a round-robin load balancer, users report being randomly logged out. Diagnose the exact failure. What REST constraint does this violate? Give two specific solutions.

Problem 2 — Design From Scratch:
Design the URL structure and HTTP methods for a task management API (users, projects, tasks, comments). Classify your design by Richardson Maturity Level. Explicitly justify why each URL is shaped the way it is.

Problem 3 — Synthesis:
A teammate says: "We should just use POST for all our API endpoints — it's simpler and we don't have to think about which verb to use." Write the technical counter-argument. Use at least three REST constraints in your answer, and give a concrete example of what breaks.

Problem 4 — Go Deeper:
Implement a minimal HATEOAS response for a payment API. The /payments/77 resource has three possible states: pending, completed, failed. Write the JSON response for each state, where the _links object reflects only the actions valid in that state (e.g., a completed payment cannot be cancelled).

Problem 5 — Connect to Previous:
From Chapter 1, you know GET requests have no body and are idempotent. From this chapter, you know GET responses should be cacheable. Explain why these three properties — no body, idempotent, cacheable — are not coincidental but are logically connected through the REST constraints. What would break if GET were allowed to have side effects?


What's Coming Next

You understand the rules. Now apply them.

Chapter 3 is where abstract constraints become concrete decisions: how do you model a real business domain as URLs? Should /users/42/orders or /orders?user_id=42 be the URL for a user's orders? How do you handle actions that don't map to CRUD (like /orders/42/cancel)? When should a resource be nested vs. flat?

Resource design is where most API arguments happen. We'll resolve them with principles, not opinions.