Konrad Kowalski (rootsher)Principal Platform & Reliability Architect110001010100000001010101001011111000101010110111

Methods and Status Codes: Semantics, Not Decoration

date
category
Backend
also in
Frontend
reading
1 min / 279 words

An API can be designed like this:

http
POST /api

{"action":"getUser","id":42}

Technically it works.

But HTTP then stops knowing that the operation is a read.

Methods are not just names of endpoint actions. They carry semantics.

Safe

GET, HEAD and OPTIONS are safe methods.

Simplified:

text
client does not request a state-changing action

That lets browsers and intermediaries perform certain optimizations, prefetching or caching without assuming a mutation.

Which is why:

http
GET /delete-user?id=42

is semantically a bad idea even when the backend accepts it.

Idempotent

An operation is idempotent if performing the same request several times has the same intended effect as performing it once.

For example:

http
DELETE /users/42

The first request removes the user.

The second may return a different status, but the intended final state is still:

text
user 42 does not exist

PUT, DELETE and the safe methods have defined idempotent semantics.

POST does not.

Why should the frontend care?

The network can fail at an ambiguous moment:

text
client -> POST /payments
server -> payment created
network breaks before response
client <- ???

The frontend does not know:

text
request failed before execution?
or
response was lost after execution?

An automatic retry may perform the operation a second time.

So a retry policy should not rest solely on:

js
catch {
  retry();
}

It has to take the semantics of the operation into account.

For non-idempotent operations applications often introduce idempotency keys of their own.

The status code is part of the contract too

This is not enough:

text
2xx = good
4xx = frontend fault
5xx = backend fault

Examples:

text
201 Created
-> a resource was created

202 Accepted
-> the request was accepted, work may continue

204 No Content
-> success with no body

304 Not Modified
-> use the existing representation from cache

401 Unauthorized
-> no valid authentication

403 Forbidden
-> the request was understood but is not allowed

409 Conflict
-> the request conflicts with the current state

For the UI, the difference between 202 and 201 can change the whole flow.

A 202 often leads to:

text
submit
  |
  v
operation accepted
  |
  v
poll / SSE / realtime update
  |
  v
operation completed

response.ok is deliberately simple

Fetch gives us:

js
response.ok

which is true for 200-299.

That is convenient, but it does not replace domain semantics.

js
if (!response.ok) {
  // we still have to know what 409, 422, 429 mean
}

HTTP gives a shared language.

The application still has to give it product meaning.

The next element of that language is headers: the metadata that tells the browser and the backend how to interpret the request and the response.