nicetool.dev logo
#http#api#backend

HTTP Status Codes for APIs: The Pairs Developers Mix Up Most

401 or 403? 400 or 422? 301, 302, 307 or 308? A practical guide to choosing the right HTTP status code for REST APIs, with the headers each one needs and the anti-patterns to avoid.

7 min read

Most APIs only use a handful of status codes, yet code reviews keep returning to the same questions. Should an expired token return 401 or 403? Is a failed validation a 400 or a 422? Why did a redirect turn a POST into a GET? Clients, proxies, browsers and search engines all react to these numbers automatically, so choosing the wrong one causes real bugs, not just a style problem.

This guide follows RFC 9110 (HTTP Semantics, 2022), which replaced the older RFC 7231 family, and focuses on the pairs that are most often confused.

The five classes in one minute

Class Meaning Who should act
1xx Informational, request continues Rarely seen in application code
2xx Success Nothing, it worked
3xx Redirection Client follows Location or uses its cache
4xx Client error Client must change the request
5xx Server error Server must be fixed; the client may retry

The 4xx/5xx split is the most important one. 4xx means "do not retry the same request", 5xx means "this may work later". Retry libraries, load balancers and monitoring alerts are all built on that assumption. Returning 500 for invalid input causes pointless retries and false alerts. Returning 400 for a database outage hides an incident.

Success codes: 200, 201, 202, 204

  • 200 OK: the general success response with a body.
  • 201 Created: a new resource was created. Include a Location header with its URL, and usually the created object in the body.
  • 202 Accepted: the request was queued but not processed yet, for example a video export. Return a way to check progress, such as a job URL.
  • 204 No Content: success with no body at all. This is a good fit for DELETE or a PUT that returns nothing. Do not send {} with 204, because some clients fail when they try to parse a body that should not exist.

401 vs 403

This is the most commonly confused pair, partly because 401 is named "Unauthorized" when it really means unauthenticated.

  • 401 Unauthorized: "I don't know who you are." The credentials are missing, expired or invalid. The specification requires a WWW-Authenticate header that tells the client how to authenticate, for example WWW-Authenticate: Bearer. A client that receives 401 should refresh its token or ask the user to log in again.
  • 403 Forbidden: "I know who you are, and you are not allowed to do this." Logging in again will not help. The user lacks the role, the plan or the ownership.

A simple test: would different credentials fix this request? If valid credentials would fix it, return 401. If this user can never do it, return 403.

There is one useful exception. When revealing that a resource exists is itself a leak, return 404 instead of 403. GitHub does this for private repositories you cannot access, so outsiders cannot discover private repository names by guessing.

400 vs 422

  • 400 Bad Request: the request could not be understood, such as malformed JSON, a wrong Content-Type or a required header that is missing.
  • 422 Unprocessable Content: the request was well-formed, but its content fails validation, for example an email without "@", an end date before the start date or a quantity below zero. RFC 9110 renamed it from "Unprocessable Entity".

Many APIs use 400 for everything, which is acceptable as long as you are consistent. Splitting them helps clients: a 400 usually means a bug in the client code, while a 422 usually means the end user typed something wrong and should see the field errors.

Two related codes: 415 Unsupported Media Type when the client sends XML to a JSON-only endpoint, and 409 Conflict when the request is valid but clashes with the current state, such as registering an email that already exists or editing a record someone else just changed.

404 vs 410

  • 404 Not Found: nothing is here, and the server is not saying whether that is permanent.
  • 410 Gone: the resource existed and was removed on purpose, permanently.

For APIs, 404 is almost always fine. For websites, 410 tells search engines more clearly that a page was deleted intentionally. Google treats the two very similarly in practice, so use 410 when you know the removal is permanent, but do not worry about converting old 404s.

Whatever you do, avoid the "soft 404": an error page that returns 200 OK. Search engines may index thousands of "Page not found" pages, and monitoring will never notice broken links.

Redirects: 301, 302, 303, 307, 308

Redirect codes differ in two ways: is it permanent, and may the client change POST into GET?

Code Permanent Method preserved
301 Moved Permanently Yes Not guaranteed (POST often becomes GET)
302 Found No Not guaranteed
303 See Other No Always becomes GET
307 Temporary Redirect No Yes
308 Permanent Redirect Yes Yes

Historically browsers changed POST to GET on 301 and 302, and the specification now allows it for compatibility. That is why 307 and 308 were introduced. In practice:

  • Moving a website to a new domain or path: 301 (or 308). Browsers and search engines cache permanent redirects, so test before you ship them.
  • Redirecting an API endpoint that receives POST or PUT: 307 or 308, so the body is not lost.
  • After a form submission, sending the user to a result page (the Post/Redirect/Get pattern): 303, which prevents duplicate submissions on refresh.

304 Not Modified

When a client sends If-None-Match with an ETag (or If-Modified-Since) and the resource has not changed, the server replies 304 with no body, and the client uses its cached copy. Most frameworks and CDNs handle this automatically for static files. For APIs, adding ETags to large, rarely changing responses saves bandwidth with very little code.

429 and 503: tell clients when to come back

  • 429 Too Many Requests: the client hit a rate limit. Send a Retry-After header (in seconds or as an HTTP date) so well-behaved clients wait instead of hammering the server.
  • 503 Service Unavailable: the server is overloaded or under maintenance. Retry-After works here too. During planned maintenance, return 503 rather than a 200 maintenance page, so search engines know it is temporary and do not replace your indexed content.

500, 502, 504: whose fault is it?

  • 500 Internal Server Error: an unexpected error in your code. Log it with enough context to debug, but never put stack traces in the response.
  • 502 Bad Gateway: a proxy or gateway (Nginx, a load balancer, an API gateway) got an invalid response from the upstream server, often because the app crashed or is not listening.
  • 504 Gateway Timeout: the proxy waited too long for the upstream server.

When you are debugging, the code tells you where to look. A 502 or 504 usually means the problem is between the proxy and your app, not inside your request handler.

Anti-patterns

  1. 200 with an error body, like {"success": false, "error": "Not found"}. Caches store it, monitoring counts it as success and client libraries do not raise an error.
  2. 500 for validation errors. It wakes up the on-call engineer for a typo in a form.
  3. A different error format for every endpoint. Use one structure. RFC 9457 Problem Details (application/problem+json) is a ready-made standard with type, title, status, detail and room for field errors:
{
  "type": "https://example.com/errors/validation",
  "title": "Invalid input",
  "status": 422,
  "errors": [{ "field": "email", "message": "must contain @" }]
}
  1. Inventing codes like 499 or 520 for your own API. They are used by specific products (Nginx and Cloudflare), and generic clients will not understand them.

And 418 I'm a teapot? It started as an April Fools' RFC in 1998. RFC 9110 now reserves the number so nobody assigns it a real meaning. It is fine as an Easter egg, but not for production errors.

Quick decision list

  • Created something → 201 + Location
  • Nothing to return → 204
  • Not logged in or token expired → 401 + WWW-Authenticate
  • Logged in but not allowed → 403 (or 404 to hide existence)
  • Broken syntax → 400; valid syntax but invalid data → 422
  • State conflict or duplicate → 409
  • Rate limited → 429 + Retry-After
  • Your bug → 500; upstream down → 502 / 504; maintenance → 503

When you meet a code you do not recognize, the HTTP Status Codes reference lists every standard code with a plain-language explanation, and you can search by number or keyword.

Written by

Nguyễn Thành Nam

Full-Stack Engineer · Backend & Platform

Full-stack engineer with 9+ years of experience building high-traffic APIs and platforms with Go, NestJS, PHP (Laravel, Phalcon) and Node.js. Specializes in GraphQL API design, real-time systems and performance optimization, and builds NICETOOL.dev for the developer community.

Try it yourself

Related articles