Status Codes

Which number to return when, and why returning 200 for everything throws away something every client already understands.

Overview

The class matters more than the number

A status code is the three-digit number at the start of every HTTP response. It tells the client, in a form machines understand, whether the request worked, and if not, whose fault it was. Clients, browsers, proxies, and monitoring tools all read it before they look at the body.

Returning 200 for everything throws that information away. A client that gets 200 with an error message inside has to parse the body to find out something the status line could have said immediately. Using the right code makes your API easier to use and easier to monitor.

This article begins with the idea that the class of the number matters more than the exact value. Codes starting with 2 mean success, 4 means the client made a mistake, and 5 means the server did. You can get most decisions right just from that.

It then picks out the success codes worth using, such as 201 for creation and 204 for a response with no content. The same treatment goes to the 4xx codes worth telling apart, including 401, 403, 404, and 409.

A dedicated section compares 422, 400, and 409, since those three cause the most debate. FastAPI uses 422 for validation, and the article explains when you should choose something different. You will leave with a short list of codes that covers nearly every real endpoint.

Before the exact code, a client reads the class.

2xx — it worked. 3xx — look elsewhere. 4xx — the caller must change a thing. 5xx — the server must.

That first digit drives real action in code you didn't write: HTTP libraries retry 5xx and not 4xx, caches store 2xx, monitoring alerts on 5xx. A client's error handling branches on it before looking at anything else.

Returning 200 with {"error": "not found"} throws all of that away. each caller now has to parse your body to discover a thing the protocol had a field for, retries do the wrong thing. Your error rate looks like zero on each dashboard.

Worth knowing

The class is the first thing a client reads: 2xx worked, 4xx the caller must change something, 5xx the server must.
status_code= sets the route default; a Response parameter lets one call differ without returning a Response object.
204 must carry no body. Declare it and return None.
401 means not authenticated — who are you? 403 means authenticated and not allowed. Clients act on the difference.
409 is for a valid request that conflicts with current state, such as a duplicate. A 422 would be wrong, because nothing about the payload was malformed.
responses= documents the non-200 shapes, which is what lets a generated client type its errors.

Status Codes

Which number to return when, and why 200-for-all discards data the client already understands.

The route's default, and overriding it

status_code on the decorator sets the default. A Response parameter lets one call differ.

example_01.pyFastAPI
Output

204 carries nothing

The one rule with teeth. Declare it and return None; anything else produces a malformed response.

example_02.pyFastAPI
Output

The named constants

fastapi.status spells every code. Readable at the call site and autocompleted, which matters for the ones you use rarely.

example_03.pyFastAPI
Output

401 and 403 are different questions

One means “I do not know who you are”, the other “I do, and no”. Clients act on the difference.

example_04.pyFastAPI
Output

409 for a conflict with current state

Not a validation failure and not a missing thing: the request was fine and the world disagrees.

example_05.pyFastAPI
Output

Documenting the ones that are not 200

responses= puts the failure shapes in the schema, so a generated client knows what an error looks like.

example_06.pyFastAPI
Output

The 2xx ones worth using

200 OK for a good read or update that returns a thing.

201 Created when a thing new exists. Usually with a Location header pointing at it.

202 Accepted when you've taken the request but not finished it. A queued job. The body should say how to check on it.

204 No Content for a success with nothing to say. A delete, often.

204 has the one rule with teeth: no body at all. Declare status_code=204 and return None. Returning a value makes a malformed response. Some clients will error on it while others silently skip the body, which is worse.

The 4xx ones worth distinguishing

400 Bad Request — malformed in a way your own code found.

401 Unauthorized — badly named: it means *unauthenticated*. I don't know who you're. Should carry a WWW-Authenticate header.

403 Forbidden — I know who you're, and no.

404 Not Found — no such thing.

409 Conflict — the request was fine and conflicts with current state: a copy, a version mismatch, an action already taken.

410 Gone — it existed and on purpose doesn't any more. Rare. Handy when you want callers to stop asking.

422 Unprocessable Entity — well-formed and didn't fit the declared shape. FastAPI makes this on its own and you rarely raise it.

429 Too Many Requests — rate limited. Should carry Retry-After.

The 401/403 gap is the one most often merged. It's worth keeping. A client seeing 401 should prompt for credentials or refresh a token. One seeing 403 shouldn't. Retrying with the same identity will fail again. Merging them makes a login loop where there should be an error message.

The 404/403 choice has a security dim. Returning 403 for a resource that exists but isn't yours confirms it exists. For anything sensitive, 404 for both is the safer answer. on purpose. Always, or the timing gives it away anyway.

422 versus 400 versus 409

These three get confused, and the rule is about *who found the problem*.

422 — check found it from your declared types and constraints. auto. You don't write it.

400 — your code found the request was malformed in a way the schema couldn't express.

409 — the request was fully valid and the current state makes it hopeless.

A copy slug is 409, not 422: nothing about the payload was wrong. The same payload would have succeeded a minute earlier.

Setting them

status_code= on the decorator sets the route's default, and that default appears in the docs.

For one call to differ, declare a Response param and assign to response.status_code. That keeps output and the response model, unlike returning a Response object.

HTTPException(status_code, detail) raises one, and it accepts headers=. Which is how you attach WWW-Authenticate to a 401 or Retry-After to a 429.

Use the status module rather than bare ints. status.HTTP_409_CONFLICT is readable and autocompleted. 409 needs the reader to know it.

Documenting the failures

response_model shows the success case only. all else is undocumented unless you say so:

responses={404: {"model": Problem, "description": "No such module"}}

Now the schema shows the error shape. A made client can type it. Without this, consumers know they'll get *a thing* on failure and have to discover what by causing one.

Worth doing for the failures a caller is expected to handle. 404 on a lookup, 409 on a create. Not worth doing for each conceivable code.

Mistakes people make with Status Codes

200 with an error body. The one that costs most. Retries, caches, monitoring and each client's error handling branch on the status class. A 200 tells all of them the call succeeded.

A body on a 204. Malformed. Some clients error, others silently skip it. This Is worse because it works until it doesn't.

Merging 401 and 403. A client seeing 401 re-authenticates. Seeing 403 it shouldn't. Merging them makes a login loop where there should be a message.

422 for a missing resource. The request was well-formed. Nothing with that id exists, which is 404.

422 for a copy. The payload was valid and would have worked a minute earlier. That is 409.

500 for a caller's mistake. If they could have avoided it, it's a 4xx. A 500 should mean your code failed, and should page somebody.

Bare ints. 409 needs the reader to know it. status.HTTP_409_CONFLICT doesn't, and your editor ends it.

The header a status implies

a few codes are incomplete without a header. Omitting it makes a strictly-correct response in practice useless.

401 should carry WWW-Authenticate, naming the scheme. Without it a client knows it must authenticate and not how.

429 should carry Retry-After. Without it a client's only plan is guessing. This Often means retrying at once and making things worse.

405 should carry Allow, listing the methods that do work.

201 usually carries Location, pointing at what was created. A client doesn't have to make the URL itself.

HTTPException takes headers= for just this. It's the sort of detail that splits an API somebody enjoys using from one they merely handle.

Choosing between 404 and 403

A choice with a security dim, worth making on purpose.

Returning 403 for a resource that exists but isn't yours confirms that it exists. For a sequential id that is an enumeration oracle. A caller can walk the range and learn how many records you've and which ids are real.

Returning 404 for both cases hides that, at the cost of a slightly less helpful message for a legitimate user who has truly lost access.

For anything sensitive - other users' data, private docs, anything under a permissions model - 404 for both is the safer default. For an inner API where enumeration tells an attacker nothing they don't already have, 403 is friendlier.

Whichever you choose, be steady. Returning 403 at times and 404 other times leaks just the data the 404 was meant to hide. Timing differences will give it away even if the status doesn't.

Redirects, briefly

The 3xx codes come up less in an API than in a website, and two are worth recognising.

307 preserves the method and body. 308 is its permanent equal. These are the ones FastAPI uses for the trailing-slash redirect. The reason it matters is that some clients still drop the body, turning a POST into an empty one.

301 and 302 once allowed clients to change the method to GET on redirect. This Is why they're the wrong choice for anything that isn't a plain read.

If an API needs to move a resource permanently, 308 with a Location header is the honest answer. More often the better answer is to keep the old path working and doc the new one.

The ones you will not write

Some codes exist and are made by infrastructure rather than by your handlers.

502, 503 and 504 come from a proxy or load balancer when your app is unreachable, overloaded or slow. Seeing them in production means the problem is in front of your code, not in it.

413 may be returned by a reverse proxy before a request reaches you. This Is why an upload limit belongs there and in the handler.

Knowing which layer makes which saves time when a thing breaks: a 500 is yours, a 502 isn't.

A closing thought

A status code is the smallest piece of an API and the one most consumed by machines.

each client library, cache, proxy, gateway and dashboard reads it. None of them read your response body. Returning the accurate one costs a keyword arg and buys correct action from all of them.

Returning 200 for all costs nothing to write and moves the work onto each caller, forever.

A short reference

Created a thing: 201, with Location. Deleted a thing: 204, no body. Queued a thing: 202, with a way to check. Read a thing: 200.

Caller sent nonsense: 422 if check caught it, 400 if you did. Not signed in: 401 with WWW-Authenticate. Signed in, not allowed: 403 - or 404 if existence itself is sensitive. Not there: 404. Conflicts with current state: 409. Asking too often: 429 with Retry-After.

Your code failed: 500, logged in full, returned as a plain message with an id.

Status Codes: summary

The class is what a client reads first. It drives retries, caching and alerting in code you didn't write. Returning 200 with an error body discards all of it.

201 for created, 204 for done-with-nothing-to-say and no body, 202 for accepted-but-not-finished. 401 for unauthenticated and 403 for not-allowed - they're other questions and clients act on the gap. 404 for missing, 409 for a valid request that conflicts with current state, 422 for a payload that didn't fit.

Set the route default with status_code=, vary it through a Response param, attach the headers a status implies. Doc the failures with responses=.

Where to go after Status Codes

The mechanics behind most of the codes above: HTTPException, custom exception handlers. The setup that keeps HTTP concerns out of the code that does the actual work.

Questions people ask about Status Codes

Which status code should a successful POST return? 201 Created when it makes a resource, ideally with a Location header. Use 200 when it only runs an action.

When do I use 204? For success with nothing to return, such as a DELETE. The response must carry no body.

What is the difference between 401 and 403? 401 means the caller is not authenticated, while 403 means they are authenticated but not allowed.

What is the difference between 400, 409, and 422? 422 is a validation failure, 409 is a conflict with current state such as a duplicate, and 400 is a general bad request.

How do I use constants instead of numbers? Import status from fastapi and write status.HTTP_201_CREATED.

Should I return 404 or 403 for a resource the user can't see? Return 404 if revealing that it exists would leak information, and 403 when the user is allowed to know it exists.

A final note

Status codes are also a form of docs that no one has to read.

An endpoint returning 201 with a Location header has told a caller that a thing was created and where to find it, without a sentence of prose. One returning 200 with a body they must inspect has told them nothing. The prose now has to exist.

Check yourself

0 of 4

Answer without scrolling back up.

  1. Why is returning 200 with an error body a problem?

  2. What must a 204 response contain?

  3. A signed-in reader tries an admin-only delete. Which code?

  4. A create request is valid but the slug already exists. Which code?

Cheat sheet

Status Codes

Return the right HTTP status code for each outcome, compare it with 200-for-everything, and see the information the wrong choice discards.

FASTAPI · vizlearn.in/fastapi/status_codes.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.