Pick the right HTTP status code for every outcome and return one consistent, safe error body.
Why it matters
Every HTTP response starts with a three-digit status code. Client code branches on it long before it reads the body: a mobile app shows "please log in" on a 401, retries on a 503, and highlights form fields on a 400. If your API answers 200 OK with { "error": "not found" } inside, every client has to parse the body to learn that something went wrong, and tools such as monitoring dashboards, caches and retry logic will treat the failure as a success.
The core idea: five families
The first digit tells you the family:
| Range | Family | Who should act |
|---|---|---|
1xx | Informational | Rare in APIs; you can ignore them |
2xx | Success | Nobody: it worked |
3xx | Redirection | The client should look somewhere else |
4xx | Client error | The caller must change the request |
5xx | Server error | The server failed; the request may be fine |
The 4xx/5xx split is the most useful thing on this page. A 4xx says "do not retry the same request, it will fail again". A 5xx says "this is our fault; retrying later might work".
The codes you will use every week
Success
200 OK: a normal successful response with a body (GET, or an update that returns the new item).201 Created: aPOSTcreated something. Return the new item, and optionally aLocation: /courses/43header.204 No Content: success with no body, common forDELETE.
Client errors
400 Bad Request: the request is malformed or fails validation (missing field, wrong type, invalid JSON).401 Unauthorized: the caller is not authenticated. No session, expired token, bad credentials. The name is historical; read it as "unauthenticated".403 Forbidden: the caller is known but is not allowed to do this. A student trying to delete a course.404 Not Found: there is no such resource at this path.409 Conflict: the request clashes with the current state. Signing up with an email that is already taken, or approving a report that was already resolved.413 Payload Too Large: the body is bigger than the server accepts.422 Unprocessable Content: some APIs use this instead of400for "valid JSON, but the values break the rules". Pick one convention and stick to it.429 Too Many Requests: rate limited. Often sent with aRetry-Afterheader.
Server errors
500 Internal Server Error: an unexpected bug or crash.502 Bad Gateway/503 Service Unavailable/504 Gateway Timeout: something between the client and your code, or a service your code depends on, is down or slow.
Worked example: choosing the code
Walk through a PATCH /courses/42 request that renames a course:
No session cookie or token -> 401
Logged in, but role is "student" -> 403
Course 42 does not exist -> 404
Body is {"title": ""} -> 400 (title must not be empty)
Title already used by another course -> 409
Database is down -> 503 (or 500)
Everything worked -> 200 with the updated course
Notice the order of the checks: authenticate first, then authorize, then look up the resource, then validate. If you validate before you authenticate, an anonymous caller can probe your rules. Some APIs return 404 instead of 403 for private items so that outsiders cannot even learn that the item exists; that is a deliberate privacy choice.
A consistent error body
Status codes say what kind of failure happened; the body says what to fix. Choose one error shape and use it everywhere:
{
"error": "Validation failed.",
"details": [
{ "field": "title", "message": "Title is required." }
]
}
In Express, a handler sends it like this:
if (!title) {
return res.status(400).json({
error: "Validation failed.",
details: [{ field: "title", message: "Title is required." }],
});
}
Some teams follow RFC 9457 ("Problem Details for HTTP APIs"), which defines standard fields such as type, title, status and detail with the content type application/problem+json. That is a good default if you have no house style.
What not to put in an error
Error messages are read by whoever calls your API, including attackers. Keep them helpful but boring:
- No stack traces or SQL in responses. Log them on the server instead.
- No account enumeration. "No user with that email" versus "Wrong password" tells an attacker which emails are registered. A login endpoint should give one message, such as "Invalid email or password.", for both.
- No secrets in any message, header or log line.
Common mistakes
200for everything, with anerrorfield in the body.500for bad input. A missing field is the caller's mistake (400), not a server crash. A route that throws on bad input usually means validation is missing.- Mixing up
401and403. Ask: "Do we know who this is?" No means401; yes but not allowed means403. - Different error shapes per route, so clients need a parser per endpoint.
- Swallowing errors. Catching an exception and returning
200hides real bugs from you and your monitoring.
Quick recap
2xxworked,4xxthe caller must change something,5xxthe server failed.- Learn
200,201,204,400,401,403,404,409,429and500by heart. - Check in order: authenticate, authorize, find the resource, validate, then act.
- Use one error body shape, keep messages useful, and never leak internals.
The Do tab has a quiz of real scenarios: pick the right code for each.