Intro TL;DR: This post is a collection of a few things I needed to clear up (to myself or others) about HTTP Status Codes while building a RESTful API. It might not make sense for an architectural style that does not rely that much on the HTTP specification.
It is probably safe to say that most (HTTP-based) web applications and services design their server APIs to follow REST guidelines to some extent. The ones that don’t either build their API as exclusively function calls over HTTP (RPCs), or use an architectural style that introduces an extra layer of abstraction that enables additional, more complex features than what the standard HTTP protocol offers (e.g. GraphQL, SOAP). All of those don’t rely that much on HTTP functionalities. Most use only
POST, or just
POST requests, and represent their responses with the basic
x00 response codes. Some even represent client errors as a successful HTTP request because the error handling is done in another layer of the application. Personally, there is nothing I hate seeing more in the Network tab than a request which is labeled as "200" but has an error in its payload, but for some architectures that is a valid approach.
Keeping all of that in mind, this post will focus on HTTP response status codes from the perspective of building a RESTful API. Additionally, I will not be going into the nitty-gritty of every response status that exists or explain all the cases when a certain status should or should not be used. If you’re searching for a good overview of all the status codes, I advise checking out MDN’s page on the topic. My goal is to capture some common mistakes and sources of confusion that spark discussions, PR rejections, and frantic googling by my coworkers and myself.
When in doubt, stick to the basics
Before I start with specific examples, I would just like to note that there is nothing wrong with using only the base
x00 status codes to represent your responses if you are unsure what to use. All of the other ones are just extensions of those that were introduced to cover common use cases, such as wasting days on building a really creative page for 404 errors.
x00 status codes are the basic ones a few others are really common and should be used when the situation calls for it. Not using them in these situations can cause frontend developers to be unsure what exactly happened. This sometimes means implementing extra logic or doing additional requests to check the status of a resource which could have been determined if a more exact status code was used.
If a resource has been created on the target collection use
201 (Created). While a
200 response on such an endpoint should be obvious, if your system features some kind of "upsert" or "soft delete reactivation" logic for any resource, receiving
200 instead of
201 can cause confusion if something other than a standard "create" happened.
If something does not exist on the target route, please return
404 (Not Found). A base
400 error can cause a developer to retry or search for the bug in all the wrong places, especially if it's a
DELETE request. There is nothing worse than triple-checking your request body trying to figure out what you did wrong and then discovering that the target object never existed in the first place.
Operation (probably) successful but not finished
This is the textbook example of when to use
202 (Accepted). It is usually used for long operations that you don't want the client to wait for to finish, or operations that depend on an external service which you do not have control over when it is executed (SMS and other messaging services are a good example here).
This response code basically says to the client “listen, you’ve done good on your end, you can’t count on how this will go, check back on the resource later”. To a developer, this is useful because it makes them aware of the fact that an error can still happen in the processing, so the request is still a good candidate to check while error hunting in case a resource validation isn’t fully implemented or there are parts of the system (external services you pay big money for) prone to having issues.
401 vs. 403
A lot of confusion arises between
401 (Unauthorized) and
403 (Forbidden). If you got it right the first time great for you, but I've seen a project have to refactor its auth middleware 2-3 times because of these (mostly because it used an error library that has an error class called NotAuthorizedError representing a
403 status, causing a lot of mixups with the Unauthorized class).
Long story short:
401is the authentication error, thrown if the client needs to go through some login process and add those credentials to the request.
403is the authorization error, thrown if the client has the login credentials, but does not have the correct role or set of permissions to execute an operation on or view a resource.
A side note on this one, don’t use
404 to "hide" resources that are available to different users. You can handle them the same way on the frontend if you want, but for the sake of your testers and developer colleagues, tell them if they are missing a permission. It makes stuff much more clear. Honestly, I'd advise showing the difference between
403 to the end-user as well because it makes bug reports a lot faster to solve.
When to use 409 (Conflict)
409 (Conflict) has a nice name that can mean a lot of things for a developer. When a developer discovers the existence of this status, they will most probably use it in the wrong use case. This response is clearly defined to be used if there is a state mismatch between the client request and server state.
In practice, this means there isn’t a locking mechanism implemented on a resource, and two or more clients can open the same resource for edit at the same time. Client A submits its updates via a
PUT request which successfully updates the target resource, while client B is still on the edit screen. Client B sends a request to update the resource, but the server notices some inconsistencies between the resource in the request and the server's new (updated by client A) resource. The server then can't or won't update the resource but will prompt the user to try again by sending a
409 error. And that's it, that's the use case for this error code, no other types of conflicts should be represented by this status.
You can be a bit more strict or liberal on what counts as “state” (e.g. what data subset is important for the request and which can be safely ignored), but an error indicating that there is a conflict between resource A and resource B in some unique property (e.g. they have the same ID property) isn’t a
409 error but a standard
400 (Bad Request). This example might seem a bit out of the blue, but the name "Conflict" can cause developers to think that the status is a good way to represent such an error. Remember, the
409 status only relates to state conflicts between server and client on the (same) target resource, nothing more.
If you ever need or want to implement some kind of optimistic concurrency control for a resource, this status code is perfect for representing error responses in case of a rollback.
What is 204 (No Content)
I saved this one for last because a discussion about this response code is what sparked the idea for writing this whole thing.
204 (No Content) response code is pretty simple. It's used as a successful response for update (usually
PUT) requests, it returns no body, and indicates to the client that the edits to the target resource have been saved and that the user can continue with what they were doing, i.e. the client can and remain on the same (edit) page. This response is the default option for implementing a "save and continue" mechanism where the client can treat its own resource state as in sync with the server after saving.
The key characteristic of this response status is that it must not have a response body, that’s why it’s called “No Content”. This is great to improve performance while working on a large edit form where you can save changes mid-way. Not having to return the entire resource on every save can reduce response time by skipping data serialization and deserialization, reducing the response size to practically nothing, and potentially skipping some extra server-side fetch logic (e.g. database calls) for data needed only for building the resource view model.
However, because there is no response body representing the updated resource, the assumption is that the client’s resource state is completely correct and in sync with the server’s resource state. Therefore, for a server to return
204, the server MUST accept all the edits exactly as the client sent them in the request. If this requirement cannot be met, you should use a standard
200 response status with the changed resource described in the response body. Otherwise, the client can assume that its state is in sync with the server, and at that point, you're just asking for all kinds of trouble.
I hope I’ve cleared up some confusion you might have had with the described status codes, or that I’ve helped you to make a decision about something you were unsure about using. If you’re still confused about something, or wish to know more, I highly encourage you to check out MDN’s page on HTTP response status codes. If that does not help, check out the RFC linked on each of the codes’ pages, they are a lot more readable than their design (or lack thereof) gives off.
Also, these rules exist for developers to utilize built-in web mechanics (such as caching), maintain consistency, and make API usage more readable and easier. The last point is extra important APIs open to external systems. However, if you have a good reason for breaking some rule, then do it. Just note explicitly what is happening in your documentation and inform your colleagues why you made this design decision.
And finally the real summary of the points in this post:
- use the base
x00statuses when unsure
- new resource =>
- no resource =>
- request good, but operation not done =>
- missing credentials =>
- wrong role / missing permission =>
- don’t use
- the target resource was updated (by someone else) while editing =>
- there is no use case for
409other than the one above
- resource successfully updated, no need for body =>
- don’t use
204if the client state can become inconsistent