We arrive at the conceptual core of the course. In the previous lesson we learned how HTTP works; now we will see how it is used well, according to the architectural style Roy Fielding described in 2000. REST is not a library you install or a specification you validate against: it is a set of constraints that, if you accept them, hand you scalability, independent evolution and simplicity; and if you ignore them, leave you with an API that is called REST but behaves like something else entirely. In this lesson we will take the six constraints apart one by one, applied to Aroma Store, and pin down three concepts that are constantly confused: resource, identifier and representation.

Contents

  1. What REST is (and what it is not)
  2. Constraint 1: client-server
  3. Constraint 2: stateless
  4. Constraint 3: cacheable
  5. Constraint 4: layered system
  6. Constraint 5: uniform interface
  7. Constraint 6: code on demand (optional)
  8. Resource, identifier and representation
  9. What an API really gains from meeting each constraint
  10. What "RESTful" means and why almost no API is entirely so

  1. What REST is (and what it is not)

REST stands for Representational State Transfer. The name, which sounds cryptic, describes the mechanism precisely: the client and the server exchange representations of the state of some resources. When you request GET /v1/coffees/cof_001, you do not receive "the coffee" (which is a row in a database and some sacks in a warehouse): you receive a JSON representation of its state at that moment.

It is essential to understand what category of thing REST is:

REST is REST is not
An architectural style: a set of design constraints A protocol (that is HTTP)
Independent of any specific technology A standard with a specification to validate against
A model derived from why the web scales A synonym for "JSON over HTTP"
Applicable with varying degrees of fidelity A library or a framework

There is no "REST validator", nor a conformance certificate. There are constraints, and an API meets them to a greater or lesser extent. That gradation is precisely what the Richardson maturity model measures, the subject of the next lesson.

Fielding defined six constraints: five mandatory and one optional. Each one removes design possibilities, and in exchange for that sacrifice you obtain desirable properties. It is a conscious trade-off.

graph TD
    R["REST<br/>architectural style"] --> C1["1. Client-server"]
    R --> C2["2. Stateless"]
    R --> C3["3. Cacheable"]
    R --> C4["4. Layered system"]
    R --> C5["5. Uniform interface"]
    R --> C6["6. Code on demand<br/><i>optional</i>"]
    C5 --> U1["Identification of resources"]
    C5 --> U2["Manipulation through representations"]
    C5 --> U3["Self-descriptive messages"]
    C5 --> U4["HATEOAS"]

  1. Constraint 1: client-server

Statement: the user interface and the data storage are separated into distinct components that communicate through an agreed interface.

This separates two worlds with different rhythms and responsibilities:

  • The client deals with presentation and the user experience.
  • The server deals with the data, the business rules and their integrity.

In Aroma Store, this means that:

  • The website can be completely redesigned without touching the server.
  • Aroma Mobile can publish a new version while the backend stays the same.
  • The backend can migrate from MySQL to PostgreSQL without any client noticing.

The practical rule: if a change in visual appearance forces you to modify the API, the separation is broken. A classic symptom is a field such as buttonColour or homeFeaturedText in a coffee's response: that is presentation leaking into the contract.

// ✗ Presentation has leaked into the API
{ "name": "Ethiopia Yirgacheffe", "formattedPrice": "€14.50", "cssClass": "featured-red" }

// ✓ The API gives data; the client decides how to display it
{ "name": "Ethiopia Yirgacheffe", "priceEuros": 14.50, "currency": "EUR", "featured": true }

In the second version, the client decides whether to format it as €14.50 or 14,50 € depending on the user's locale, and what a featured product looks like. The API supplies the fact, not the form.

  1. Constraint 2: stateless

Statement: every client request must contain all the information needed to be understood. The server stores no session context between requests.

Here it is worth distinguishing two types of state precisely:

Type of state Where it lives in REST Example in Aroma Store
Resource state (application state) On the server, persistently The order ord_5001 exists and is paid
Session state (client state) On the client, travelling in every request Who I am, which page of the catalogue I am on

The Aroma Store cart is a good case for sharpening your understanding. The cart is stored on the server, but not as a "session": it is a resource with its own identity, /v1/carts/crt_77. The client keeps only its identifier and sends it when needed. The difference is subtle but decisive: a resource can be queried, shared between devices and survive a server restart; an in-memory session cannot.

Let's compare two designs:

# ✗ With session state on the server: the second request depends on the first
POST /v1/cart/select-customer   # the server "remembers" the customer in its memory
POST /v1/cart/add               # which cart? it depends on what came before

# ✓ Stateless: each request is self-sufficient
POST /v1/carts/crt_77/items
Authorization: Bearer <token of customer cus_842>
{ "coffeeId": "cof_001", "quantity": 2 }

In the second version, the request identifies the cart in the URL, the customer through the token and the content in the body. Any server in the group can handle it with no prior knowledge.

What you gain:

  • Horizontal scalability: adding servers is trivial, there are no sessions to synchronise.
  • Fault tolerance: if one server goes down, the next request is served by another with no loss of context.
  • Visibility: any intermediary can understand a request in isolation, which makes caches, firewalls and monitoring possible.

What you pay: each request is larger, because it repeats the credentials and the context. With HTTP/2 header compression the cost is smaller than it looks.

  1. Constraint 3: cacheable

Statement: every response must state, explicitly or implicitly, whether it is cacheable and for how long.

In Aroma Store, not all data changes at the same rate:

Resource Does it change often? Reasonable policy
Coffee catalogue Rarely Cache-Control: public, max-age=300
A coffee's detail Little (stock, more so) max-age=60 + ETag for revalidation
A customer's order It is private and critical Cache-Control: no-store
A coffee's reviews Little public, max-age=600

Here is how it looks in practice:

GET /v1/coffees HTTP/1.1
Host: api.aromastore.example

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=300
ETag: "a7f3c9"

The server is saying two things: "you can reuse this response for 300 seconds without asking me" and "this version of the content has the fingerprint a7f3c9". After those 5 minutes, the client can ask whether it has changed by sending that fingerprint, and if it has not changed the server responds 304 Not Modified with no body: a saving in bandwidth and time.

None of this is exclusive to your server: browsers, CDNs, proxies and gateways understand these headers out of the box. That is the great gift of building on HTTP instead of inventing your own mechanism. The whole caching mechanism is studied in lesson 04-06.

Watch out for the risk: caching private data as public is a security breach. If a proxy caches one customer's order and serves it to another, you have leaked personal data.

  1. Constraint 4: layered system

Statement: the architecture is composed of hierarchical layers; each component knows only the immediate layer it interacts with, not the whole topology.

The Aroma Store client believes it is talking to "the API". In reality, its request crosses several layers:

graph LR
    C["Aroma Mobile"] --> CDN["CDN / cache"]
    CDN --> GW["API Gateway<br/>auth, limits, metrics"]
    GW --> LB["Load balancer"]
    LB --> S1["API server 1"]
    LB --> S2["API server 2"]
    S1 --> DB[("Database")]
    S2 --> DB

Neither end knows the full chain: the client does not know how many servers there are, and the application server does not know whether the request came from a CDN or directly. That allows you to:

  • Insert a gateway that centralises authentication and usage limits without changing a single line of the client (lesson 05-06).
  • Put a CDN in front to serve the catalogue from locations near the user.
  • Add or remove servers according to load.
  • Rewrite an internal service without anyone outside noticing.

The trade-off is the extra latency of each hop and the difficulty of debugging: that is why observability matters so much (lesson 04-07), along with the correlation headers that let you follow a request across every layer.

  1. Constraint 5: uniform interface

This is the central constraint, the one that distinguishes REST from any other style. The idea: instead of every service inventing its own way of speaking, they all use the same generic interface. It breaks down into four sub-constraints.

6.1. Identification of resources

Every resource has a unique, stable identifier, its URI:

https://api.aromastore.example/v1/coffees              -> the collection of coffees
https://api.aromastore.example/v1/coffees/cof_001      -> one specific coffee
https://api.aromastore.example/v1/orders/ord_5001      -> one specific order
https://api.aromastore.example/v1/coffees/cof_001/reviews -> that coffee's reviews

The URI identifies a thing, not an action. This explains why /v1/getCoffee?id=1 or /v1/createOrder are not REST URIs: they name verbs, not nouns. We will work through the specific rules for designing URIs in lesson 02-02.

6.2. Manipulation of resources through representations

The client does not modify the resource directly: it sends a representation of the state it wants, and the server decides whether to apply it.

PUT /v1/coffees/cof_001 HTTP/1.1
Content-Type: application/json

{
  "name": "Ethiopia Yirgacheffe",
  "origin": "Ethiopia",
  "roast": "light",
  "priceEuros": 15.00,
  "stock": 95
}

The client says: "I want the coffee cof_001 to end up like this". The server validates, checks permissions, applies business rules and decides. It may accept (200), reject because the data is invalid (422) or because of missing permissions (403). The client proposes, the server disposes.

6.3. Self-descriptive messages

Each message contains the information needed to be interpreted on its own, with no external knowledge:

POST /v1/orders HTTP/1.1
Host: api.aromastore.example
Content-Type: application/json     <- the body's format is declared
Accept: application/json           <- what I expect to receive, declared
Authorization: Bearer ...          <- who I am, declared

HTTP/1.1 201 Created               <- the result, in a standard code
Content-Type: application/json     <- the response format, declared
Location: /v1/orders/ord_5001      <- where it ended up, declared

An intermediary that has never heard of coffee can still understand that something was created and where. That is why using the correct status codes and declaring content types properly matters: it is what lets generic infrastructure do its job.

6.4. Hypermedia as the engine of application state (HATEOAS)

The response includes links indicating what can be done next:

{
  "id": "ord_5001",
  "status": "pending_payment",
  "totalEuros": 29.00,
  "_links": {
    "self":   { "href": "/v1/orders/ord_5001" },
    "pay":    { "href": "/v1/orders/ord_5001/payment", "method": "POST" },
    "cancel": { "href": "/v1/orders/ord_5001", "method": "DELETE" },
    "customer": { "href": "/v1/customers/cus_842" }
  }
}

The client does not need the addresses or the business rules hard-coded: it discovers that this order can be paid or cancelled because the server tells it so. If the order had already been shipped, the cancel link would simply not be there.

It is the most ignored sub-constraint in all of REST, and the one that generates the most debate. We will study it thoroughly in the next lesson, 01-05.

  1. Constraint 6: code on demand (optional)

Statement: the server may temporarily extend the client's functionality by sending it executable code.

It is the only optional constraint of the six. The canonical example is a web page that sends JavaScript to the browser: the browser did not know how to validate that form and the server sends it the code to do so.

In REST APIs it is barely used, and with good reason: sending executable code to a client is a considerable security risk and breaks simplicity. Aroma Store will not use it. It is enough to know that it exists and why it is optional: it reduces the system's visibility, and that is why Fielding left it out of the mandatory set.

  1. Resource, identifier and representation

These three concepts are the foundation of everything, and confusing them is the cause of most bad designs.

Concept Definition Example
Resource Anything with identity and interest for the business The coffee Ethiopia Yirgacheffe
Identifier (URI) The unique, stable address of that resource /v1/coffees/cof_001
Representation A concrete way of expressing its state at a given moment A JSON, XML or HTML document

The key point: a resource can have many representations, and none of them is the resource. The same /v1/coffees/cof_001 can be returned in different formats depending on what the client asks for:

GET /v1/coffees/cof_001 HTTP/1.1
Accept: application/json

HTTP/1.1 200 OK
Content-Type: application/json

{"id":"cof_001","name":"Ethiopia Yirgacheffe","priceEuros":14.50}
GET /v1/coffees/cof_001 HTTP/1.1
Accept: application/xml

HTTP/1.1 200 OK
Content-Type: application/xml

<coffee><id>cof_001</id><name>Ethiopia Yirgacheffe</name><priceEuros>14.50</priceEuros></coffee>

Same resource, same identifier, two representations. This mechanism is called content negotiation and is the subject of lesson 02-05.

Three practical consequences of this distinction:

  1. The representation does not have to mirror the database table. The "coffee" resource may be assembled from three internal tables, and the representation may omit internal fields such as supplierCostEuros.
  2. There can be different representations for different contexts. The listing may return a summarised version and the detail a complete one. It is still the same resource.
  3. A resource is not necessarily a data entity. It can be a concept or a process: /v1/coffees/best-selling is a perfectly legitimate resource even though no "best-selling" table exists.

  1. What an API really gains from meeting each constraint

The constraints are not bureaucracy: each one buys a specific property.

Constraint What it forces you to give up What you get in return
Client-server Mixing presentation and data Independent evolution on each side; several clients on one backend
Stateless Keeping a session in server memory Horizontal scalability, fault tolerance, visibility
Cacheable Treating every response the same Less latency, less load, less cost
Layered system Letting the client know the topology Being able to insert gateways, CDNs and load balancers without breaking anything
Uniform interface Inventing your own semantics Generic tooling that works without knowing your domain
Code on demand (optional) Extensible clients, at the cost of visibility and security

The overall price is real: a uniform interface is less efficient than an interface tailored to one specific case. Fielding acknowledges this explicitly. The bet is that, at internet scale and over the long run, generality is worth more than local optimisation. When that bet does not pay off — extremely high-performance internal communication, or clients that need tailored data — gRPC and GraphQL appear, which we will see in 01-07.

  1. What "RESTful" means and why almost no API is entirely so

An API that follows the REST style is called RESTful. In practice, the term is used very loosely. These are the most common violations:

Frequent practice Constraint it breaks Why it happens
URIs with verbs: /v1/createOrder Uniform interface (identification) People think in functions, not resources
Everything by POST, including queries Uniform interface + cacheable Convenience, or a hangover from SOAP
Returning 200 OK with {"error": ...} Self-descriptive messages Fear that the client "will not know how to handle" a 4xx
Sessions in server memory Stateless The traditional web model is carried over
Responses with no links at all HATEOAS Implementation cost and clients that do not make use of it
Ignoring Cache-Control Cacheable The mechanism is not known

The honest conclusion: most APIs called REST are, in reality, "well-organised HTTP APIs". They meet the important structural constraints (client-server, stateless, resources with URIs, correct verbs and codes) but do not implement HATEOAS.

Is that a problem? It depends on the context, and it deserves a nuanced answer:

  • The serious violations do matter: keeping a session on the server stops you scaling; using POST to read leaves you with no caching; returning 200 for errors breaks monitoring. These are measurable technical costs.
  • The absence of HATEOAS is debatable: it contributes less when you control every client and its life cycle.

What matters is that you know what you are breaking and why. A conscious decision not to implement HATEOAS is professional; not knowing that it exists is not. To measure precisely where your API sits on that scale, there is a tool we will look at right now.

Common Mistakes and Tips

  • Believing that using JSON and HTTP is already REST. In practice it is a necessary condition, but nowhere near a sufficient one.
  • Confusing "stateless" with "no data". The server stores persistent resources; what it does not store is conversation context between requests.
  • Modelling actions as resources by default. If you end up with /v1/coffees/cof_001/updatePrice, the verb belongs in the method (PATCH), not in the URI.
  • Putting presentation in the responses. Pre-formatted strings, translated text or CSS classes tie the API to one specific client.
  • Caching private data as public. It is a leak of personal data waiting to happen.
  • Arguing about REST purity instead of solving problems. The goal is a useful, maintainable, evolvable API, not winning an argument.
  • Tip: when you are unsure about a design, ask yourself "what resource is this and what am I doing to it?". If the answer can be expressed with a noun and one of the HTTP methods, you are on the right track.

Exercises

Exercise 1: identify the broken constraint

For each design, state which REST constraint is broken and propose a correct alternative:

  1. POST /v1/searchCoffees with body {"origin":"Ethiopia"}.
  2. The API stores the cart in the server's memory after POST /v1/startCheckout, and subsequent requests depend on it.
  3. GET /v1/coffees responds 200 OK with {"success": false, "message": "service down"}.
  4. A coffee's response includes "priceHtml": "<span class='sale'>€14.50</span>".

Exercise 2: resource, identifier and representation

Aroma Store wants to expose the "sales report for the current month". Answer:

  1. Is this a legitimate resource even though no table called reports exists?
  2. Propose its URI.
  3. Propose two different representations of the same resource and how the client would request them.
  4. Should it be cacheable? Justify your answer.

Exercise 3: redesign an API that is not RESTful

A team has delivered this API for managing reviews. Redesign the four operations respecting the REST constraints and explain each change.

POST /v1/api?action=newReview        body: {review}
POST /v1/api?action=listReviews      body: {coffeeId}
POST /v1/api?action=deleteReview     body: {reviewId}
POST /v1/api?action=editRating       body: {reviewId, rating}

Solutions

Solution 1

  1. Uniform interface and cacheable. A search is a read, and using POST with a verb in the URI prevents caching and is misleading. Alternative: GET /v1/coffees?origin=Ethiopia. (Note: when the search criteria are enormous and do not fit in a URL, a POST to a search resource is an accepted and conscious exception.)
  2. Stateless. The server keeps context between requests, which prevents scaling and breaks if the node goes down. Alternative: the cart is a resource, POST /v1/carts returns crt_77, and subsequent requests go to /v1/carts/crt_77/items with the identifier made explicit.
  3. Self-descriptive messages. A server failure must be signalled with 503 Service Unavailable; returning 200 deceives caches, retries and monitoring, which will believe everything is fine.
  4. Client-server. Presentation (HTML and CSS classes) invades the data contract. Alternative: {"priceEuros": 14.50, "onSale": true}, and let each client decide how it is rendered.

Solution 2

  1. Yes, it is a legitimate resource. A resource is anything with identity and interest for the business; it does not have to correspond to a table. The report is a perfectly identifiable concept.
  2. Proposed URI: /v1/reports/sales?period=2026-08. The path identifies the type of report and the parameter narrows the period. An equally valid and more "resource-like" alternative: /v1/reports/sales/2026-08.
  3. Two representations: JSON for the internal panel (Accept: application/json) and CSV so the finance team can open it in a spreadsheet (Accept: text/csv). Same resource, same identifier, a different representation negotiated by header.
  4. Yes, but carefully. It is an expensive calculation that does not change every second, so Cache-Control: private, max-age=600 is reasonable: it is reused for ten minutes but marked private because it is sensitive information that no shared cache should store. The report for a month that has already closed could be cached for far longer.

Solution 3

POST   /v1/coffees/cof_001/reviews   body: {"rating":5,"comment":"..."}  -> 201 Created
GET    /v1/coffees/cof_001/reviews                                       -> 200 OK
DELETE /v1/reviews/rev_101                                               -> 204 No Content
PATCH  /v1/reviews/rev_101           body: {"rating":4}                  -> 200 OK

Changes and their justification:

  • The single /v1/api endpoint and the action parameter disappear. Each resource has its own identifiable URI: the resource identification sub-constraint is met.
  • The verb moves from the URL to the HTTP method. POST creates, GET reads, DELETE removes, PATCH modifies partially. Now intermediaries understand the intent without knowing the domain.
  • Listing reviews becomes a GET, which makes it safe, idempotent and cacheable: a change with a direct impact on performance and cost.
  • Identifiers move out of the body and into the URI, because they identify the target resource rather than being data belonging to the operation.
  • A coffee's reviews hang off the coffee (/v1/coffees/cof_001/reviews), which expresses the relationship in the structure itself; to operate on one specific review, its own URI is enough.
  • Meaningful status codes are added: 201 with a Location header on creation, 204 on deletion (there is nothing to return).

Conclusion

REST is an architectural style, not a protocol: six constraints — client-server, stateless, cacheable, layered system, uniform interface and code on demand — that give up a certain design freedom in exchange for scalability, independent evolution and compatibility with all the generic infrastructure of the web. We have seen that the uniform interface is its heart, with its four sub-constraints, and we have pinned down the distinction between resource (the thing), identifier (its address) and representation (the concrete form in which it travels). We have also admitted honestly that most real APIs break something, above all HATEOAS, and that the professional stance is to know what you are breaking and why.

What is missing is a tool for measuring that "how much" precisely. In the next lesson, The Richardson Maturity Model and HATEOAS, we will work through the model's four levels by rewriting the same Aroma Store operation — creating an order — at each of them, we will look at real hypermedia formats such as HAL and JSON:API, and we will discuss without dogmatism when it is worth reaching level 3.

REST API Course: Principles of Designing and Developing RESTful APIs

Module 1: Introduction to RESTful APIs

Module 2: Designing RESTful APIs

Module 3: Building RESTful APIs

Module 4: Best Practices and Security

Module 5: Tools and Frameworks

Module 6: Case Studies and Projects

© Copyright 2026. All rights reserved