Lesson 5 / 27
Choosing Status Codes
Use the standard classes and a handful of precise codes consistently.
The class tells who is at fault
Status codes group by first digit: 2xx success, 3xx redirect or "not modified", 4xx the client made a mistake, 5xx the server failed. Use precise codes: 200 OK, 201 Created (with a Location header) after creating, 202 Accepted when work continues in the background, 204 No Content for success with no body, 400 Bad Request for malformed input, 401 Unauthorized (not authenticated), 403 Forbidden (authenticated but not allowed), 404 Not Found, 409 Conflict (state clash, such as a duplicate), 412 Precondition Failed (failed conditional request), 422 Unprocessable Content (well-formed but invalid data), 429 Too Many Requests (with Retry-After) and 503 Service Unavailable. Never return 200 with an error hidden inside the body: clients, caches and monitors rely on the code.
Say what happened, clearly
Status codes, error bodies and pagination are where an API either feels trustworthy or confusing.
Event to status code, run
I ran this plain-Python example. A tiny lookup that you could use as a team reference: created 201, accepted-for-async 202, deleted 204, validation 422, conflict 409, rate limited 429.
import hmac, hashlib, json, time, bisect
def status_for(event):
return {"created": 201, "accepted_async": 202, "deleted": 204, "validation": 422, "conflict": 409, "not_found": 404, "unauth": 401, "forbidden": 403, "rate": 429}[event]
print([status_for(e) for e in ("created", "accepted_async", "deleted", "validation", "conflict", "rate")])
Output:
[201, 202, 204, 422, 409, 429]
401 vs 403
401 means "I do not know who you are" (missing or bad credentials); 403 means "I know who you are and you may not do this". Mixing them confuses clients and hides bugs.
Quick check: A user is logged in but lacks permission for an action. Which code fits?
- 401 Unauthorized
- 403 Forbidden
- 200 OK
- 500 Internal Server Error
Answer
403 Forbidden — The identity is known but not allowed, which is 403.