Skip to main content
ANVISoftware Solutions
Lesson 3 of 18Beginner16 min

Status Codes

By the end of this lesson

Return the code that accurately describes what happened.

The status code is the first thing a caller reads, and often the only thing it branches on. Get it right and a client can handle your response with three lines of code. Get it wrong and every caller has to inspect the body to work out what happened.

The first digit gives the category: 2xx worked, 3xx go somewhere else, 4xx the caller did something wrong, 5xx the server did. That split carries real information. A 4xx tells a caller that retrying the same request unchanged will fail again. A 5xx tells it that retrying might work.

The codes that cover almost every endpoint you will write:

200 OK
It worked and there is a body — typically a read, or an update that returns the updated record.
201 Created
A new resource exists. Include a Location header pointing at it, so the caller learns the address it did not choose.
202 Accepted
You have taken the work but have not done it yet. Only use this when the work really is asynchronous, and tell the caller how to check progress.
204 No Content
It worked and there is deliberately nothing to send back. Common for DELETE and for PUT.
400 Bad Request
The request itself is wrong — malformed JSON, a missing required field, a string where a number belongs.
401 Unauthorized
You have not established who you are. The name is misleading; read it as unauthenticated.
403 Forbidden
You are authenticated and this is still not allowed.
404 Not Found
There is nothing at this address.
409 Conflict
The request is understandable but clashes with the current state — a duplicate email, an order already cancelled, a stale version.
422 Unprocessable Content
The request parsed correctly and still cannot be processed, because a rule about its content fails.
500 Internal Server Error
Something broke that you did not anticipate. This is a report on your code, not on the caller.

The distinctions people get wrong

401 and 403 are the pair most often swapped. The question each one answers is different, and clients treat them differently: a 401 is worth reacting to by refreshing a token and trying again, while a 403 never is.

 401 Unauthorized403 Forbidden
The question it answersWho are you?I know who you are — may you do this?
Typical causeNo token, an expired token, or a token that failed validationA valid token without the role, scope or ownership the endpoint requires
Can the caller fix it by signing in again?Usually yesNo. Signing in again produces the same answer
What a good client doesRefresh credentials once, then retryShow the user that the action is not available to them

400 against 422 is the next confusion. Use 400 when you could not make sense of the request: broken JSON, a required field absent, "tomorrow" in a field typed as a date. Use 422 when you understood every field and a rule about the content still fails — a start date before the department existed, a quantity of zero, a discount above the permitted band. Many teams return 400 for both, and that is a defensible simplification as long as the error body names the failing field. What is not defensible is using them inconsistently across endpoints.

404 against 204 catches beginners in a specific place: an empty list. GET /api/departments/3/employees for a department with no employees is a successful read of an empty collection, so it is 200 with an empty array, not 404 and not 204. Reserve 404 for an address with nothing behind it — a department id that does not exist. Reserve 204 for "this worked and I am deliberately sending no body".

409 covers conflicts with current state. Creating a second employee with an email address already in use is not a malformed request, so it is not 400; the address exists, so it is not 404. The request is fine and the world disagrees with it. The same code covers a concurrency clash, where the record changed after the caller read it.

Four outcomes from the same endpoint
HTTP
POST /api/employees  ->  201 Created
Location: /api/employees/57

POST /api/employees  ->  400 Bad Request
{ "code": "validation_failed",
  "errors": { "email": ["Email is required."] } }

POST /api/employees  ->  409 Conflict
{ "code": "email_in_use",
  "message": "An employee already exists with that email address." }

GET /api/employees/9999  ->  404 Not Found
{ "code": "employee_not_found",
  "message": "No employee exists with id 9999." }
  • 201 carries a Location header. Without it the caller has to guess or re-query to find what it created.
  • 400 reports a problem with the request document itself — a required field is missing, so there was nothing to validate against the rules.
  • 409 reports a problem with the world. The request was well formed and understood; the conflict is with data that already exists.
  • 404 is about the address, not about permissions or input. Note that every failure body shares one shape, which is the subject of a later lesson in this module.

Summary

  • The first digit tells a caller whether retrying unchanged could ever work
  • 401 means "who are you"; 403 means "I know, and no"
  • 400 is a malformed request, 422 breaks a rule about well-formed content, and 409 conflicts with existing state
  • An empty collection is 200 with an empty array, never 404
  • Never report failure with 200 — it hides the failure from every tool that reads the status line

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Think about it

Choose the code

State the status code for each, with a one-line reason.

1. GET /api/employees/9999 where no such employee exists. 2. GET /api/departments/3/employees where the department exists but is empty. 3. POST /api/orders with no Authorization header. 4. DELETE /api/employees/42 by an authenticated caller whose role does not permit deletion. 5. POST /api/employees with an email address already in use. 6. DELETE /api/employees/42 that succeeds and returns nothing.

Show solution

1 is 404 — there is nothing at that address.

2 is 200 with an empty array. The read succeeded; the collection is empty, which is a fact about the data rather than a failure.

3 is 401. The caller has not said who it is, so the question of permission has not been reached.

4 is 403. Identity was established and the answer is still no, which is why signing in again would not help.

5 is 409. The request is well formed and understood, and it conflicts with data that already exists.

6 is 204. It worked, and there is deliberately no body — the caller does not need the record it just removed.

Try it yourself

Find the dishonest responses

Open an API you have built or have access to, and send three deliberately wrong requests: one with a missing required field, one with no credentials, and one for an id that does not exist.

Record the status code each returns. Note any that answer 200, or that answer 500 for a caller mistake.

Show solution

The three expected answers are 400, 401 and 404. Anything answering 200 is hiding failures from every monitoring tool in the path, and anything answering 500 for a caller mistake will generate alerts nobody can act on.

A 500 for a missing field usually means the code reached a null reference before any validation ran, so the wrong status code is pointing at a real gap rather than a cosmetic one.

Knowledge check

Nothing is recorded and there is no score. The explanation appears either way.

A caller sends a valid token, and the endpoint requires a role the caller does not hold. Which code is correct?
Why is 200 with an error message in the body worse than returning the wrong 4xx code?

Saved in this browser only.