Backend / REST APIs / 03_status_codes.md

Status codes

Updated 5 interview angles 3 min read source
On this page5
  1. The classes, and what each means to a client
  2. The pairs that get confused
  3. The ones worth knowing beyond the basics
  4. The anti-pattern
  5. Interview angle

Status codes

The status code is the part of your response that infrastructure you do not control acts on — caches, retry policies, load balancers, client libraries, monitoring. Getting it wrong is not a style problem; it changes behaviour several layers away.

The classes, and what each means to a client

Class Client should
1xx keep going, provisional
2xx accept the result
3xx look elsewhere
4xx not retry — fix the request
5xx retry, possibly later

That 4xx/5xx line is the one that matters operationally. A retry policy keys on it, an alert threshold keys on it, and a 500 returned for a bad input wakes someone up for a client’s typo.

The pairs that get confused

400 or 422. 400 means the server could not parse the request at all — malformed JSON, a missing required header. 422 means it parsed fine and failed semantic validation.

http
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{"type": "https://example.com/probs/validation",
 "title": "Validation failed",
 "status": 422,
 "errors": [{"field": "email", "detail": "not a valid address"}]}

FastAPI returns 422 for every Pydantic failure, which is why it is so common in Python APIs. application/problem+json is RFC 9457 (which obsoleted 7807) — one error shape across the whole API, machine-readable.

401 or 403. 401 is unauthenticated — I do not know who you are, and it must carry a WWW-Authenticate header. 403 is authenticated but not permitted. Returning 403 for a missing token is a common mix-up that sends clients hunting for a permissions problem that does not exist.

python
if token is None:
    raise HTTPException(401, headers={"WWW-Authenticate": "Bearer"})
if not user.can(action):
    raise HTTPException(403)

Gotcha: for a resource the caller may not even know exists, 404 is often the right answer instead of 403. A 403 confirms the resource is there, which is an information leak in a multi-tenant system.

200 or 201 or 204. A create returns 201 with a Location header so the client learns the new URI. A successful call with nothing to say returns 204 and no body. 200 with an empty object is neither.

http
HTTP/1.1 201 Created
Location: /orders/9c3f

The ones worth knowing beyond the basics

Code Meaning Why it matters
409 conflict with current state duplicate create, version clash
410 gone, permanently tells crawlers to forget, unlike 404
412 precondition failed optimistic concurrency via If-Match
428 precondition required force clients to send If-Match
429 rate limited must carry Retry-After
503 unavailable deliberate shedding; carry Retry-After
504 upstream timed out distinct from your own 500

429 and 503 without Retry-After are the missed opportunity: a well-behaved client will back off correctly if you tell it how long, and will guess badly if you do not.

The anti-pattern

json
HTTP/1.1 200 OK
{"success": false, "error": "insufficient funds"}

Every cache stores it, every retry policy treats it as success, every dashboard counts it as healthy, and every generic HTTP client’s raise_for_status() does nothing. The error is invisible to everything except code that specifically looks for it.

Interview angle 5

  • “400 or 422?” - 400 for a request the server cannot parse; 422 for well-formed syntax that fails semantic validation. FastAPI returns 422 for Pydantic failures, which is why it appears so often in Python APIs.
  • “401 or 403?” - 401 means unauthenticated and must carry WWW-Authenticate; 403 means authenticated but not permitted. For a resource in another tenant, 404 is often better than 403, because 403 confirms it exists.
  • “What should a create return?” - 201 with a Location header. 409 for a conflict with existing state, 422 for validation. Returning 200 with an error body is the anti-pattern, because clients, caches and monitoring all read it as success.
  • “Which 5xx codes matter operationally?” - 503 with Retry-After for deliberate load shedding and 504 for an upstream timeout. Both tell a client to retry and roughly when; a bare 500 tells it nothing and invites an immediate retry.
  • “Why does the exact code matter if the body explains it?” - because infrastructure you do not control reads only the code. Retry policies, caches, load balancers and alerting never look at the body.