REST Principles & Constraints
REST was defined by Roy Fielding as a set of six constraints. An API that satisfies them earns the architectural benefits REST promises — scalability, cacheability, evolvability. You don't need to recite Fielding to build good APIs, but understanding why each constraint exists turns vague "best practices" into reasoned decisions. This page walks the constraints and the two properties — idempotency and safety — that govern correct method use.
The six constraints
Constraint | What it requires |
|---|---|
Client–server | Separate UI concerns from data storage; they evolve independently |
Stateless | Each request is self-contained; no client session on the server |
Cacheable | Responses declare whether (and how long) they can be cached |
Uniform interface | A consistent, resource-oriented way to interact |
Layered system | Clients can't tell if they talk to the server or a proxy/cache/gateway |
Code on demand (optional) | Server may send executable code (e.g. JS) — rarely used |
The uniform interface, unpacked
Identification of resources — each resource has a stable URI (
/users/5).Manipulation through representations — clients change state by sending representations (JSON), not by calling internal procedures.
Self-descriptive messages — each message carries enough metadata (method, headers, content type) to be understood on its own.
HATEOAS — responses can include links to related actions/resources (the most-skipped part in practice).
Safe and idempotent methods
Two properties decide how clients, caches, and proxies may treat a request. Safe = no observable change on the server. Idempotent = repeating the request has the same effect as doing it once.
Method | Safe? | Idempotent? |
|---|---|---|
| Yes | Yes |
| Yes | Yes |
| No | Yes |
| No | Yes |
| No | No |
| No | Not necessarily |
Idempotency in practice — PUT vs POST
// PUT is idempotent: same request, same final state. Calling it 3x = 1x.
app.put('/users/5', (req, res) => {
users.set(5, { id: 5, ...req.body }) // fully replaces user 5
res.json(users.get(5))
})
// POST is NOT idempotent: each call creates ANOTHER resource.
app.post('/users', (req, res) => {
const user = { id: nextId++, ...req.body } // a new id every time
users.set(user.id, user)
res.status(201).json(user)
})Cacheability
// Mark a rarely-changing GET as cacheable for 5 minutes:
app.get('/config', (req, res) => {
res.set('Cache-Control', 'public, max-age=300')
res.json(appConfig)
})
// Never cache user-specific or sensitive responses:
app.get('/me', (req, res) => {
res.set('Cache-Control', 'private, no-store')
res.json(req.user)
})Richardson Maturity Model
Level | Characteristic |
|---|---|
0 — "The Swamp of POX" | One URL, one method (RPC tunneled over HTTP) |
1 — Resources | Multiple resource URLs, but still one method |
2 — HTTP verbs | Proper methods + status codes (where most good APIs live) |
3 — Hypermedia | Adds HATEOAS links — "full" REST |