REST, richtig gemacht
Das Richardson-Reifegradmodell: Ressourcen, HTTP-Verben und Statuscodes korrekt genutzt - und wo HATEOAS/Hypermedia wirklich hilft gegenüber wo es übertrieben ist.
Deutsche Übersetzung in Arbeit
Diese Lektion ist noch nicht ins Deutsche übersetzt und wird daher auf Englisch angezeigt. Der Rest der Seite ist vollständig lokalisiert.
Auf dieser Seite
An API is different from internal code in one terrifying way: you can't refactor it freely, because you can't see or control who depends on it. A mobile app you don't own, a partner's integration, a script written two years ago - all coupled to your promises. That makes API design a senior discipline, and it starts with getting the fundamentals right. Most "REST" APIs aren't especially RESTful; the Richardson Maturity Model is a useful ladder for seeing how far up you actually are.
The maturity ladder
Leonard Richardson's model describes four levels of REST adoption:
- Level 0 - one endpoint, one verb. Everything is
POST /apiwith an action in the body ({"action": "getAccount"}). This is RPC-over-HTTP wearing a REST costume - HTTP is just a tunnel. - Level 1 - resources. You have multiple URIs for different things (
/accounts,/transactions) instead of one god-endpoint, but you stillPOSTeverything. - Level 2 - HTTP verbs and status codes. You use
GET,POST,PUT,PATCH,DELETEfor their real meanings and return proper status codes (200,201,404,409). This is where most good APIs live, and it's a perfectly respectable place to be. - Level 3 - hypermedia (HATEOAS). Responses include links telling the client what it can do next, so the API is self-describing and navigable.
The ladder isn't a mandate to reach Level 3; it's a lens. Aim solidly for Level 2, and adopt Level 3 where it earns its keep.
Level 2: verbs and status codes as the contract
The heart of practical REST is using HTTP's own semantics instead of inventing your own:
GET /accounts/42 → 200 with the account (safe, cacheable, no side effects)
GET /accounts/999 → 404 Not Found
POST /accounts → 201 Created + Location header (create; NOT idempotent)
PUT /accounts/42 → 200 (full replace; idempotent - same call, same result)
PATCH /accounts/42 → 200 (partial update)
DELETE /accounts/42 → 204 No Content (idempotent)Two properties matter enormously and come free if you follow the verbs:
- Safe methods (
GET) have no side effects - so proxies and browsers may cache and prefetch them. Never hide a state change behind aGET. - Idempotent methods (
GET,PUT,DELETE) produce the same result if repeated - so a client can safely retry after a timeout.POSTis not idempotent, which is exactly why creation needs idempotency keys (a later lesson).
Status codes are part of the contract too: 2xx success, 4xx the client's fault (don't retry as-is), 5xx the
server's fault (safe to retry). Returning 200 {"error": "not found"} throws all that away - the client can't
tell success from failure without parsing your prose.
Level 3: HATEOAS - and when it's overkill
At Level 3, a response carries links to related actions, so clients follow links rather than hard-coding URLs:
{ "id": 42, "balance": "100.00", "status": "ACTIVE",
"_links": {
"self": { "href": "/accounts/42" },
"freeze": { "href": "/accounts/42/freeze" }, // available because ACTIVE
"close": { "href": "/accounts/42/close" }
} }The promise is decoupling - the server can change URLs and available actions, and a link-following client adapts. Spring HATEOAS supports it. But be honest: most clients ignore the links and hard-code URLs anyway, and the extra payload and machinery often outweigh the benefit. HATEOAS shines for long-lived, widely-integrated, workflow-heavy APIs; for a straightforward internal service, Level 2 is the pragmatic sweet spot.
Verbs in URLs are the giveaway
The single most common REST mistake is putting the action in the URL: POST /accounts/42/getBalance, POST /createAccount, GET /deleteAccount?id=42. That's Level 0/1 thinking - RPC in disguise. The resource is a noun (/accounts/42); the verb is the HTTP method. If your URLs contain verbs, you're tunneling actions through POST and losing caching, idempotency, and the shared vocabulary that makes HTTP useful.
Level 0 is a single window where you shout every request - 'I'd like to order,' 'now I'd like to pay,' 'now check my bill' - and the staff parse your words each time; HTTP is just the window. Level 2 is a proper restaurant with a shared protocol everyone already knows: you point at a table (a resource URL), and standard gestures mean standard things - sitting means 'serve me' (GET), raising a hand means 'the bill' (a specific verb). Nobody re-explains the rules; the conventions carry the meaning. Level 3 adds a menu on each plate listing what you can do next. Most diners are happy with a great Level-2 restaurant; the printed-next-steps menu is a nice touch that only some venues need.
An API has these endpoints: POST /api/getUser?id=5, POST /api/updateUser, POST /api/deleteUser, and all of them
return 200 with a body like {"success": false, "reason": "not found"} on failure. Identify which Richardson level
this is at and redesign the four concerns to reach Level 2, noting the properties you gain.
At Level 2 of the Richardson Maturity Model, why do the HTTP verbs matter so much?
Key takeaways
- An API is a contract with consumers you can't see or force to upgrade, which is what makes its design and evolution a senior discipline.
- The Richardson Maturity Model is a lens: Level 0 (RPC tunnel), Level 1 (resources), Level 2 (HTTP verbs + status codes), Level 3 (hypermedia/HATEOAS).
- Aim solidly for Level 2: resources as nouns, correct verbs, and honest status codes (2xx/4xx/5xx) as part of the contract.
- Following the verbs gives you safety (cacheable, side-effect-free GET) and idempotency (retry-safe PUT/DELETE) for free; never hide a state change behind GET.
- HATEOAS (Level 3) adds self-describing links and helps long-lived, widely-integrated APIs, but is often overkill - most clients hard-code URLs anyway.