NodeJSREST Principles & Constraints

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

Five are required, one is optional
The first five constraints are mandatory for an API to be "RESTful"; **code on demand** is explicitly optional and seldom applies to data APIs. Satisfy a constraint and you gain a property: statelessness enables horizontal scaling, cacheability cuts load, the uniform interface makes the API self-consistent.
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).

HATEOAS: links in responses — aspirational, rarely fully implemented
*Hypermedia As The Engine Of Application State* means a response includes links telling the client what it can do next (e.g. an order includes a `"cancel"` URL). Pure REST mandates it; most real APIs skip or partially adopt it because clients usually hard-code URLs from docs. Know the term — it appears in interviews — but don't feel obligated to build a full hypermedia API.
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?

GET

Yes

Yes

HEAD

Yes

Yes

PUT

No

Yes

DELETE

No

Yes

POST

No

No

PATCH

No

Not necessarily

Honor these properties — clients and infrastructure assume them
Browsers, proxies, and HTTP clients rely on these guarantees: they prefetch and cache `GET`s (so a `GET` that *deletes* data is a real bug), and they safely **retry** idempotent requests after a network hiccup. If your `GET` mutates state or your `PUT` isn't idempotent, automatic retries and caching corrupt data. Design each handler to match its method's contract — don't fight it.
Idempotency in practice — PUT vs POST

JS
// 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)
})
Why retries make idempotency matter
A client sends `POST /users`, the response is lost to a timeout, and it retries — now you have two users. With `PUT /users/5` the retry is harmless: the end state is identical. For `POST` operations that must survive retries safely (payments, signups), use an **idempotency key** (a client-supplied unique header you dedupe on) — the pattern Stripe uses.
Cacheability

JS
// 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)
})
Don't let caches store private data
Caching is a REST superpower — but mark personalized or sensitive responses `private` / `no-store`, or a shared proxy/CDN may serve one user's data to another. Use `Cache-Control`, `ETag`, and `Last-Modified` deliberately: aggressive caching for public, immutable data; none for authenticated, per-user responses.
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

Level 2 is the pragmatic target
Most production APIs sit at **Level 2**: resource URLs, correct HTTP methods, meaningful status codes. Level 3 (hypermedia) is the formal ideal but rarely pays off for typical JSON APIs. Aim squarely for Level 2 — it captures nearly all of REST's practical value.
Next
Turn these principles into concrete URLs and endpoints: [Designing API Endpoints](/nodejs/designing-endpoints).