In this guide
Every time a browser or an app talks to a server, the server answers with a three-digit status code: 200, 404, 500, and a few dozen others. Students meet these codes constantly — in the browser console, in Postman, in server logs — but most learn them one error at a time, usually at 2 a.m. the night before a demo.
This guide explains the whole system at once: what the five code families mean, the individual codes you will actually meet, when your own app should return each one, and how to debug them with curl when something goes wrong.
Quick answer
HTTP status codes are grouped by their first digit: 2xx means success, 3xx means the client must go elsewhere, 4xx means the client made a mistake, and 5xx means the server failed. The codes you will meet most are 200 (OK), 201 (created), 301/302 (redirect), 400 (bad request), 401 (not logged in), 403 (not allowed), 404 (not found), and 500 (server error). A good API returns the most specific correct code, because the code is the first thing a client — and a developer debugging at midnight — reads.
How a status code travels
A status code is the first line of every HTTP response. When a browser asks for a page, the exchange looks like this:
GET /books/42 HTTP/1.1
Host: example.com
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 42, "title": "Data Structures"}
The client sent a request; the server replied with a status line, some headers, and a body. The code sits in that status line, so it is visible even when the body is empty or unreadable. That is why tools like curl show it first, and why a 404 page can still load with a perfectly rendered "not found" message — the code and the body are separate things.
The five families
| Family | Meaning | Who is at fault |
|---|---|---|
| 1xx | Informational — the request was received, keep going | Nobody; rarely seen in student apps |
| 2xx | Success — the request worked | Nobody |
| 3xx | Redirection — go to a different URL | Nobody; the resource moved |
| 4xx | Client error — the request was wrong | The client (or the user) |
| 5xx | Server error — the server failed to fulfil a valid request | The server |
The first digit is the whole diagnosis in one character. If the code starts with 4, the request itself is the problem, and re-sending the same request will fail the same way. If it starts with 5, the request was fine and the server is broken, so the fix lives in server code or configuration.
The 2xx codes: success
- 200 OK — the standard success. The request worked and the response body holds the result. Use it for GET requests and for updates that return the updated resource.
- 201 Created — a new resource was created. Use it after a successful POST that creates something, and include a Location header pointing at the new resource's URL. Returning 200 for a creation is not wrong, but 201 tells the client exactly what happened.
- 204 No Content — the request worked and there is nothing to send back. Common for DELETE requests: the item is gone, so there is no body to return.
A common beginner mistake is returning 200 for everything, including errors, and putting the real result in the body. That breaks every client that checks the status code — which is every well-written client, including browsers, fetch wrappers, and Postman test scripts.
The 3xx codes: redirection
- 301 Moved Permanently — the resource has a new permanent URL. Browsers and search engines remember this and update their bookmarks and indexes. Use it when a route changes for good.
- 302 Found — a temporary redirect. The browser follows it this time but keeps the old URL for next time. Login pages use this: an unauthenticated visitor to a dashboard is sent to the login page temporarily.
- 304 Not Modified — the client's cached copy is still fresh, so the server sends no body. This is how browsers avoid re-downloading unchanged files, and you will see it constantly in the network tab.
Students rarely need to return 3xx codes by hand — frameworks do it for them — but understanding them explains why a page sometimes "moves" and why the network tab is full of 304s on a second visit.
The 4xx codes: client errors
These are the codes you will debug most, so they deserve care.
- 400 Bad Request — the request is malformed or fails validation: a missing required field, a number where text was expected, a broken JSON body. This is the code for "your data is wrong".
- 401 Unauthorized — the request needs authentication and none was provided, or the token is expired. Despite the name, it means "not logged in", not "not allowed". The fix is to log in or refresh the token.
- 403 Forbidden — the client is logged in but is not allowed to do this. A student trying to delete another student's project gets a 403, not a 401 — the server knows who they are, and the answer is still no.
- 404 Not Found — the URL does not match any resource. Either the route does not exist, or the specific item (book 42) does not exist. This is the most common code students see, usually because of a typo in the URL or a missing route.
- 405 Method Not Allowed — the URL exists, but not for this verb: sending a POST to a route that only accepts GET. Frameworks return this automatically, and it usually means the frontend called the wrong method.
- 409 Conflict — the request conflicts with the current state: registering an email that already exists, or booking a room that is already taken. Use it when the data is well-formed but the state refuses it.
- 422 Unprocessable Entity — the request is well-formed but semantically wrong, often used for validation failures in APIs. Some frameworks use 400 for this instead; either is defensible, but pick one and be consistent.
- 429 Too Many Requests — the client has hit a rate limit. Back off and retry later. You will meet this calling public APIs with a free key.
The 401-versus-403 distinction is the one that trips up almost everyone, and it is worth memorising: 401 means "I don't know who you are", 403 means "I know who you are, and no".
The 5xx codes: server errors
- 500 Internal Server Error — something broke on the server and it does not want to say what. An unhandled exception, a crashed database connection, a bug in a route handler — all surface as 500. Never return 500 for a client mistake.
- 502 Bad Gateway — the server acting as a gateway got an invalid response from the server behind it. Common when a proxy sits in front of your app and the app itself has crashed.
- 503 Service Unavailable — the server is temporarily unable to handle the request, usually because it is overloaded or down for maintenance. Unlike 500, 503 often comes with a Retry-After header.
When your app returns a 500, the status code is not the diagnosis — it is the announcement that a diagnosis is needed. The real answer is in the server logs, and the first debugging step is always to read them.
Returning the right code from your own app
A small Flask example shows the pattern: validate first, check permissions second, do the work third, and let each failure return its own code.
@app.route("/api/books/<int:book_id>", methods=["DELETE"])
def delete_book(book_id):
book = Book.query.get(book_id)
if book is None:
return jsonify({"error": "book not found"}), 404
if book.owner_id != current_user.id:
return jsonify({"error": "not your book"}), 403
db.session.delete(book)
db.session.commit()
return "", 204
Notice the order: existence is checked before ownership, so a user probing random IDs learns nothing about which books exist. And the error body carries a short human-readable message alongside the code — the code says what kind of failure it is, the body says why, in words a developer can act on.
Two rules cover most cases. First, never return 200 with an error hidden in the body; clients check the code, and a 200 that means failure will confuse every tool in the chain. Second, keep error bodies consistent: the same shape — for example, always an object with an "error" field — on every endpoint, so the frontend can handle failures in one place.
Debugging status codes with curl
When an API call fails, curl shows the status code without any browser or frontend in the way:
curl -i http://localhost:5000/api/books/42
The -i flag prints the response headers, including the status line, so you see the code, the content type, and any redirect target. A few habits make this fast:
- 404 on a route you just wrote: check the URL spelling, the HTTP method, and whether the server restarted after you added the route. Development servers usually reload automatically; production ones do not.
- 401 after logging in: the token may not be attached to the request. Print the Authorization header you are sending and compare it with what the server expects.
- 403 for the owner of the resource: the permission check is comparing the wrong IDs — a string against an integer, or the session user against the resource owner. Log both values.
- 500 with no detail: read the server log, not the browser. The traceback names the file and line; the status code only says something broke.
- A redirect loop: the browser reports "too many redirects" when two routes send each other back and forth — a login page redirecting to itself is the classic case.
Common mistakes
- Returning 200 for errors. The code is the contract; hiding errors in the body breaks clients, tests, and monitoring.
- Mixing up 401 and 403. Not logged in is 401; logged in but not allowed is 403.
- Returning 404 for a validation failure. A bad field value is a 400; 404 is for a missing resource.
- Returning 500 for bad input. A client sending bad data is a 4xx problem; 500 means the server itself failed.
- Leaking internals in error bodies. A 500 page that prints a full traceback hands attackers your file paths and library versions. Log the detail server-side; send the client a short generic message.
- Ignoring 429. Retrying immediately after a rate-limit response just burns the quota faster. Back off, wait, and retry once.
Frequently asked questions
What is the difference between 404 and 410?
404 means the resource was not found, with no statement about whether it ever existed. 410 Gone means it existed and was deliberately removed. Search engines treat 410 as a stronger signal to drop the URL; student apps almost never need it.
Should a failed login return 401 or 400?
- The credentials were presented and rejected, which is an authentication failure. Some APIs return 400 or 404 here to avoid confirming that an account exists, but 401 is the standard choice.
Why does my API return 200 in Postman but 404 in the browser?
Usually the two tools are hitting different URLs — a missing trailing slash, a different port, or a cached route. Compare the exact URL each one sends, including the method.
What code should a search with no results return?
200 with an empty list. An empty result is a successful search that found nothing, not an error. Reserve 404 for a missing resource, like a single item fetched by ID.
Do I need to memorise all the codes?
No. Know the families and the ten codes in this guide, and look the rest up when they appear. What matters is the habit: check the code first, then read the body, then read the logs.
Limitations
Status codes describe the outcome of one request, not the whole story. A 200 does not guarantee the body is what you expected — a misconfigured server can return 200 with an error page — and a 500 does not tell you where the bug is. Codes are also a coarse language: two very different validation failures both come back as 400, and the difference lives only in the error message. Treat the code as the first clue, not the full diagnosis, and build the habit of pairing it with a clear message and a server log you can actually read.