Course outline · 0% complete

0/29 lessons0%

Course overview →

Status Codes That Tell the Truth

lesson 3-3 · ~10 min · 10/29

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:

FamilyMeaningYou will mostly use
1xxinformational(rare)
2xxsuccess200 OK, 201 created, 204 done, no body
3xxredirect301 moved permanently
4xxthe client did something wrong400, 401, 403, 404, 409, 429
5xxthe server broke500 internal error, 503 unavailable

The 4xx codes worth memorizing:

  • 400 bad request: the input is malformed or invalid.
  • 401 unauthorized: we do not know who you are (not logged in). Badly named, it really means unauthenticated.
  • 403 forbidden: we know who you are, and you are not allowed.
  • 404 not found: no such resource.
  • 409 conflict: the request clashes with current state (email already taken).
  • 429 too 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 deleteReads as
204, empty bodyit worked, nothing to say
200 with a bodyit worked, here is something
404there was nothing to delete
403it 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.