REST Architectural Constraints — What Makes an API Truly RESTful
Fast recall
Quick revision
Core answer
Whether you know REST is an architectural style, not just HTTP+JSON
Equating 'REST' with 'uses HTTP and JSON' — missing statelessness and HATEOAS
Mention Richardson Maturity Model (Level 0-3) and that most APIs are Level 2 (resources + HTTP verbs), not Level 3 (HATEOAS)
Interview-ready guide
Interview answer
REST is not a protocol or a library — it is an architectural style defined by six constraints. The term is widely misused: most APIs described as 'REST APIs' are actually HTTP APIs that partially follow REST principles.
The constraints that matter in interviews
- Stateless — every request contains all information needed to process it; the server holds no session state; this constraint is what enables horizontal scaling without sticky sessions
- Uniform interface — resources identified by URI, manipulation through representations, self-descriptive messages, and HATEOAS; the uniform interface is what defines REST, not just using HTTP
- Layered system — client cannot tell if it is talking directly to the server or through a proxy, load balancer, or CDN; enables transparent infrastructure changes
Richardson Maturity Model
- Level 0: single URI, all operations via POST (SOAP-style)
- Level 1: separate resources (
/orders,/customers) - Level 2: HTTP verbs (GET, POST, PUT, DELETE) + correct status codes — this is what most 'REST APIs' actually implement
- Level 3: HATEOAS — responses contain links to related actions; clients discover capabilities rather than hardcoding URIs
Resource-based URIs represent things, not actions: /orders/123/cancel is action-based; /orders/123 with PATCH {"status": "cancelled"} is resource-based and the correct pattern.
REST Architectural Constraints — What Makes an API Truly RESTful
Direct Answer
REST has 6 architectural constraints: client-server, stateless, cacheable, uniform interface, layered system, and optional code-on-demand. Most 'REST APIs' are actually HTTP APIs — true REST requires HATEOAS which few implement.
Code Example
Roy Fielding's dissertation defined six constraints for REST: client-server, stateless, cacheable, uniform interface, layered system, and code-on-demand (optional). Most "REST APIs" satisfy only two or three. Here is an endpoint that satisfies them all.
@RestController
@RequestMapping(value = "/orders", produces = MediaType.APPLICATION_JSON_VALUE)
public class OrderController {
private final OrderService service;
private final OrderMapper mapper;
// ---- Uniform interface: resource URL + standard HTTP verbs + HATEOAS links ----
@GetMapping("/{id}")
public ResponseEntity<EntityModel<OrderDto>> get(@PathVariable Long id,
WebRequest request) {
Order order = service.find(id);
// ---- Cacheable: ETag + conditional GET ----
String etag = """ + order.getVersion() + """;
if (request.checkNotModified(etag)) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED).eTag(etag).build();
}
EntityModel<OrderDto> model = EntityModel.of(mapper.toDto(order),
linkTo(methodOn(OrderController.class).get(id, null)).withSelfRel(),
linkTo(methodOn(OrderController.class).cancel(id)).withRel("cancel"));
return ResponseEntity.ok()
.eTag(etag)
.cacheControl(CacheControl.maxAge(30, TimeUnit.SECONDS).cachePublic())
.body(model);
}
// ---- Stateless: every request carries its own auth; no session ----
@PostMapping
public ResponseEntity<OrderDto> create(@Valid @RequestBody OrderRequest req,
@RequestHeader("Idempotency-Key") String key,
@AuthenticationPrincipal Jwt jwt) {
Order order = service.createIdempotent(req, key, jwt.getSubject());
URI location = linkTo(methodOn(OrderController.class).get(order.getId(), null)).toUri();
return ResponseEntity.created(location).body(mapper.toDto(order));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> cancel(@PathVariable Long id) {
service.cancel(id);
return ResponseEntity.noContent().build();
}
}
# A fully-formed request/response conversation
GET /orders/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi... ← stateless: request carries identity
Accept: application/json
If-None-Match: "7" ← cacheable: conditional GET
HTTP/1.1 200 OK
ETag: "7"
Cache-Control: public, max-age=30
{
"id": 42, "status": "PAID", "total": 99.90,
"_links": { ← uniform interface: HATEOAS
"self": { "href": "/orders/42" },
"cancel": { "href": "/orders/42" }
}
}
- Client-server: UI and API evolve independently; the server exposes resources, not UI widgets.
- Stateless:
Authorizationheader on every request; no server session. A request replayed to any instance must behave identically. - Cacheable:
ETag+Cache-Controllet proxies and browsers skip the server entirely. A304 Not Modifiedis the single cheapest response on the internet. - Uniform interface:
GET /orders/{id}not/getOrder?id=42; standard status codes (201,204,404,409);_linkslet clients follow relationships without hard-coding URLs (HATEOAS). - Layered system: clients cannot tell whether the response came from a CDN, an API gateway, the Spring app, or a cache — each layer is transparent.
- If the API does not satisfy these, it is RPC over HTTP, not REST — which is fine, but call it what it is.
Common Mistakes & Pitfalls
Equating 'REST' with 'uses HTTP and JSON' — missing statelessness and HATEOAS
Check your understanding
Answer these without looking back at the article.
- What is HATEOAS and is it worth implementing?
- Why is statelessness important for scaling?
- What is the difference between PUT and PATCH?
Related interview prep categories
1Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.