REST API Fundamentals · Lesson 3 of 30
REST Architecture Constraints and the Richardson Maturity Model
"RESTful" is used loosely – often it just means "JSON over HTTP". Strictly, REST is a set of constraints: rules an architecture follows to get scalability, simplicity and evolvability. Knowing them helps you make better design decisions and answer a very common interview question properly.
In this lesson
- The six REST constraints and what each buys you
- Statelessness in practice
- The Richardson Maturity Model
- HATEOAS – hypermedia links, shown live
The six constraints
| Constraint | Rule | Benefit |
|---|---|---|
| Client–server | UI and data storage are separate | clients and servers evolve independently |
| Stateless | each request carries all the context it needs | any server can handle any request – easy horizontal scaling |
| Cacheable | responses say whether they can be cached | fewer requests, faster clients, less server load |
| Uniform interface | resources with URLs, standard methods, self-descriptive messages, hypermedia | any client can talk to any REST API the same way |
| Layered system | clients can't tell if they talk to the server or an intermediary | load balancers, CDNs, gateways and caches can sit in between |
| Code on demand (optional) | servers may send executable code | rarely used for APIs |
Statelessness in practice
Stateless means the server keeps no client session between requests – not that the application has no state. Data lives in the database; who the user is travels with every request:
# stateless: every request identifies itself
GET /api/cart HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
# not RESTful: the server must remember what "next" means for this client
GET /api/orders/next-page HTTP/1.1
Why stateless scales
- Request 1the load balancer sends it to server A
- Request 2goes to server B – fine, it carries its own token and parameters
- Server A restartsno client notices; nothing was stored in its memory
The Richardson Maturity Model
Leonard Richardson described four levels of "how RESTful" an HTTP API is:
From RPC over HTTP to full REST
- Level 3 – Hypermediaresponses include links to related actions and resources (HATEOAS)
- Level 2 – HTTP verbsGET, POST, PUT, DELETE and proper status codes – where most good APIs are
- Level 1 – Resourcesmany URLs (/orders/1), but everything is still POST
- Level 0 – One endpointPOST /api with an "action" in the body – RPC tunnelled over HTTP
# Level 0 # Level 2
POST /api {"action": "getOrder", "id": 7} GET /api/orders/7
POST /api {"action": "cancelOrder", …} DELETE /api/orders/7
HATEOAS: links in responses
Hypermedia As The Engine Of Application State means a response tells the client what it can do next, through links – like a web page's links tell you where you can go. The client follows links instead of hard-coding every URL:
// server.js
import express from 'express';
const app = express();
const orders = { 7: { id: 7, status: 'pending', total: 1499 }, 8: { id: 8, status: 'shipped', total: 899 } };
function withLinks(order) {
const self = `/api/orders/${order.id}`;
const links = { self: { href: self }, items: { href: `${self}/items` } };
if (order.status === 'pending') {
links.pay = { href: `${self}/payment`, method: 'POST' }; // only offered while pending
links.cancel = { href: self, method: 'DELETE' };
}
if (order.status === 'shipped') {
links.track = { href: `${self}/tracking` };
}
return { ...order, _links: links };
}
app.get('/api/orders/:id', (req, res) => {
const order = orders[req.params.id];
if (!order) return res.status(404).json({ error: 'Order not found' });
res.json(withLinks(order));
});
app.listen(process.env.PORT ?? 3000);
$ curl http://localhost:3000/api/orders/7
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"id": 7,
"status": "pending",
"total": 1499,
"_links": {
"self": {
"href": "/api/orders/7"
},
"items": {
"href": "/api/orders/7/items"
},
"pay": {
"href": "/api/orders/7/payment",
"method": "POST"
},
"cancel": {
"href": "/api/orders/7",
"method": "DELETE"
}
}
}
$ curl http://localhost:3000/api/orders/8
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"id": 8,
"status": "shipped",
"total": 899,
"_links": {
"self": {
"href": "/api/orders/8"
},
"items": {
"href": "/api/orders/8/items"
},
"track": {
"href": "/api/orders/8/tracking"
}
}
}The pending order offers pay and cancel; the shipped one offers track instead. The server – not every client – owns the rules about what's allowed. Formats such as HAL (the _links style used here) and JSON:API standardise this. In practice, many production APIs stop at Level 2, but links for pagination and related resources are common and very useful.
Be pragmatic
Aim for solid Level 2: meaningful resource URLs, correct methods and status codes, statelessness and cache headers. Add hypermedia where it removes real coupling – pagination links, state-dependent actions – rather than for its own sake.
Practice
Try it yourself
Rewrite these Level 0 calls as Level 2 requests:
POST /api {"action":"listInvoices"},POST /api {"action":"getInvoice","id":5},POST /api {"action":"deleteInvoice","id":5}.Show solution
GET /api/invoices GET /api/invoices/5 DELETE /api/invoices/5
Quick check
Which constraint lets any server in a cluster handle any request?
Statelessness.
Which Richardson level uses HTTP verbs and status codes?
Level 2.
What does HATEOAS add?
Links in responses describing what the client can do next.
Summary
- REST = client–server, stateless, cacheable, uniform interface, layered system (+ optional code on demand).
- Richardson levels: one endpoint → resources → HTTP verbs → hypermedia.
- Most good APIs are Level 2; add hypermedia links where they reduce coupling.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.