The five families
The status code is the one part of your response that machines act on without human help: browsers decide whether to show or redirect, caches decide whether to store, client code decides whether to retry, and monitoring dashboards decide whether to page someone at night. Pick a wrong code and that machinery misbehaves, a cache stores your error page, or an uptime monitor sleeps through an outage.
Status codes group by their first digit:
| Family | Meaning | You will mostly use |
|---|---|---|
| 1xx | informational | (rare) |
| 2xx | success | 200 OK, 201 created, 204 done, no body |
| 3xx | redirect | 301 moved permanently |
| 4xx | the client did something wrong | 400, 401, 403, 404, 409, 429 |
| 5xx | the server broke | 500 internal error, 503 unavailable |
The 4xx codes worth memorizing:
400bad request: the input is malformed or invalid.401unauthorized: we do not know who you are (not logged in). Badly named, it really means unauthenticated.403forbidden: we know who you are, and you are not allowed.404not found: no such resource.409conflict: the request clashes with current state (email already taken).429too many requests: slow down (rate limiting, unit 8).
A correct status code is not cosmetic. Clients, caches, browsers, and monitoring tools all change behavior based on it.
A duplicate email on signup
A POST /signup with a syntactically valid body whose email is already registered should answer 409 conflict.
The request itself is well-formed, so 400 is not quite right. The problem is a clash with existing state, which is precisely what 409 means, and stating that lets the client show a useful message instead of a generic failure.
The near-misses each lie about something. 500 would falsely blame the server for a situation the server handled correctly, 403 is about permissions rather than state, and 200 would report success for a signup that did not happen.
The distinction between 400 and 409 is worth holding onto, since it comes up constantly. 400 means the server could not make sense of the request, and 409 means the request made sense and cannot be carried out given what already exists.
Choosing the closest truthful code is a skill interviewers do probe, and the reason is practical rather than pedantic. A client can retry a 500 and should never retry a 409, so the code decides what the caller's automatic behavior will be.
A successful delete with nothing to say
When DELETE /notes/7 succeeds and there is nothing useful for the body, the response is 204 no content, meaning success with a deliberately empty body.
204 exists precisely for the case where it worked and there is nothing to report, and clients skip body parsing entirely when they see it. Sending a body with a 204 is a protocol violation rather than a style choice.
404 would be lying, because the request succeeded even though the resource is now gone. A later DELETE of the same id would correctly answer 404, and that difference is exactly the information the two codes carry.
The 200-with-body option works and sends filler data that every client must ignore, such as {"success": true}, which is the status code repeated in a less useful form. The convention is 204.
| Response to a delete | Reads as |
|---|---|
| 204, empty body | it worked, nothing to say |
| 200 with a body | it worked, here is something |
| 404 | there was nothing to delete |
| 403 | it exists and you may not |
Note that a delete returning 200 with the deleted object is a legitimate design when the client needs the data for an undo feature. The rule is to have a reason for the body, since 204 is the default when no reason exists.
Mapping situations to codes
statusFor turns a described situation into the code that tells the truth about it.
function statusFor(situation) { switch (situation) { case "created": return 201; case "no such resource": return 404; case "server bug": return 500; case "not logged in": return 401; case "logged in but not allowed": return 403; case "invalid input": return 400; default: return 200; } } const cases = [ "created", "not logged in", "logged in but not allowed", "invalid input", "no such resource", "server bug", ]; for (const c of cases) console.log(c + " -> " + statusFor(c));
Output
created -> 201 not logged in -> 401 logged in but not allowed -> 403 invalid input -> 400 no such resource -> 404 server bug -> 500
The three added cases are the ones people confuse. 401 asks who you are, 403 says you may not, and 400 says your input is broken, and each of those is a different conversation with the client.
The 401 naming is genuinely misleading and worth saying twice. It is spelled "unauthorized" in the specification and means unauthenticated, so an anonymous request gets 401 and a logged-in request without permission gets 403.
default: return 200 makes success the fallback, which is a reasonable choice for a helper like this and a dangerous default for real handlers. A server that answers 200 for anything it did not explicitly classify reports failures as successes.
The switch is a lookup dressed as control flow, and an object mapping situations to codes would work equally well. The switch earns its place here because each case is a one-line return, and past a dozen entries the object form reads better.
Note that this function contains no logic beyond the mapping, which is deliberate. Deciding what happened is the handler's job, and turning that decision into a number is a separate concern that belongs in one place.
Deleting someone else's post
A logged-in user sending DELETE /posts/9 for a post that exists and belongs to a different user should get 403 forbidden.
The server knows who the user is, so it is not 401, the post exists, so it is not 404, and the request is valid, so it is not 400. The only problem is permission, which is what 403 describes.
Working through the elimination is the reliable way to pick a 4xx code. Each candidate corresponds to one thing being wrong, and here exactly one thing is wrong, so exactly one code fits.
Some APIs deliberately return 404 instead, to avoid revealing that the post exists. That is a defensible privacy choice, especially for private resources where the id itself is sensitive, and it trades a small amount of honesty for not confirming existence to a stranger.
The textbook answer is 403, and the deciding question in practice is whether existence is a secret. A public blog post that someone cannot delete is fine to acknowledge with 403, and a private document in another company's account is usually better hidden behind 404.
This exact scenario returns in lesson 9-3, where the notes API enforces ownership on listing and deleting. The code chosen there is the same reasoning applied to real handler code.