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
- What REST is (and what it is not)
- Constraint 1: client-server
- Constraint 2: stateless
- Constraint 3: cacheable
- Constraint 4: layered system
- Constraint 5: uniform interface
- Constraint 6: code on demand (optional)
- Resource, identifier and representation
- What an API really gains from meeting each constraint
- What "RESTful" means and why almost no API is entirely so
- 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"]
- 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.
- 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.
- 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.
- 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.
- 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, declaredAn 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.
- 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.
- 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:
- 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. - 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.
- A resource is not necessarily a data entity. It can be a concept or a process:
/v1/coffees/best-sellingis a perfectly legitimate resource even though no "best-selling" table exists.
- 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.
- 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
POSTto read leaves you with no caching; returning200for 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:
POST /v1/searchCoffeeswith body{"origin":"Ethiopia"}.- The API stores the cart in the server's memory after
POST /v1/startCheckout, and subsequent requests depend on it. GET /v1/coffeesresponds200 OKwith{"success": false, "message": "service down"}.- 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:
- Is this a legitimate resource even though no table called
reportsexists? - Propose its URI.
- Propose two different representations of the same resource and how the client would request them.
- 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
- Uniform interface and cacheable. A search is a read, and using
POSTwith 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, aPOSTto a search resource is an accepted and conscious exception.) - 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/cartsreturnscrt_77, and subsequent requests go to/v1/carts/crt_77/itemswith the identifier made explicit. - Self-descriptive messages. A server failure must be signalled with
503 Service Unavailable; returning200deceives caches, retries and monitoring, which will believe everything is fine. - 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
- 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.
- 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. - 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. - Yes, but carefully. It is an expensive calculation that does not change every second, so
Cache-Control: private, max-age=600is reasonable: it is reused for ten minutes but markedprivatebecause 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 OKChanges and their justification:
- The single
/v1/apiendpoint and theactionparameter 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.
POSTcreates,GETreads,DELETEremoves,PATCHmodifies 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:
201with aLocationheader on creation,204on 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
- What Is an API?
- History and Evolution of APIs
- HTTP Fundamentals for APIs
- Basic Principles of REST
- The Richardson Maturity Model and HATEOAS
- REST vs. SOAP
- REST Compared with GraphQL, gRPC and Webhooks
Module 2: Designing RESTful APIs
- RESTful API Design Principles
- Resources and URIs
- HTTP Methods
- HTTP Status Codes
- Representations, Headers and Content Negotiation
- Filtering, Sorting, Pagination and Search
- API Versioning
- API Documentation
Module 3: Building RESTful APIs
- Setting Up the Development Environment
- Building a Basic Server
- Handling Requests and Responses
- Input Data Validation
- Persistence and the Data Access Layer
- Authentication and Authorisation
- Error Handling
- Testing and Validation
Module 4: Best Practices and Security
- API Design Best Practices
- Security in RESTful APIs
- OAuth 2.0 and OpenID Connect in Practice
- Rate Limiting and Throttling
- CORS and Security Policies
- HTTP Caching and Performance
- Observability: Logs, Metrics and Traces
Module 5: Tools and Frameworks
- Postman for API Testing
- Swagger and OpenAPI for Documentation
- Popular Frameworks for RESTful APIs
- Contracts, Mocks and Automated API Testing
- Continuous Integration and Deployment
- API Gateways and Developer Portals
