Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Honestly, my controversial take is that for APIs, it would be cleaner to not use any HTTP status codes other than 200 and have all of the semantics in the body of the response. I'm sure someone smarter than me will jump in and explain why this wouldn't work in practice, but it just feels like application semantics are leaking from a much more natural location in the body of the response. I feel similarly about HTTP request methods other than POST in APIs; between the endpoint route and the body, there should be more than enough room to express the difference between POST, PATCH, and DELETE without needing them to be encoded as separate HTTP methods.


I'm sympathetic, but this can have issues if you want your API to be used by anything other than your own client, including stuff like logging middleware. A lot of tools inherently support/understand HTTP status codes, so building on top of that can make integration a lot easier.

We, very roughly, do it like this:

- 200: all good

- 401: we don't know who you are

- 403: you're not allowed to do that

- 400: something's wrong and you can fix it

- 500: something's wrong and you can't fix it

Each response (other than 401) includes a json blob with details that our UI can do something with, but any other consumer of the API or HTTP traffic still knows roughly what's going on.

I've worked in places where we really sweated on getting the perfect HTTP status codes, and I'm not sure it added much benefit.

On POST - I find myself doing logical GETs with POST a lot, because the endpoint requires more information than can be conveyed in URL params. It makes me feel dirty, and it's obviously not RESTful but you know - sometimes you just have to get things done.


You've just described basically everything a dev needs to know to implement HTTP APIs that report status codes properly, yet some people still seem to think it's oh so complicated. What has gone wrong?


I can understand how people might look at all the full list status codes and think it's all too hard, but yes, once you realize that there are only a handful you need most of the time it all becomes pretty simple.


Sure, but the problem in my opinion is that while the handful that you pick is totally reasonable, someone else might pick a slightly different handful that's just as reasonable. If I want to use a new API and delete a user, how do I know if it uses DELETE or POST, and if it will return 401 or 403? At best, you'll be able to skim through the documentation more quickly due to having encountered similar conventions before, but nothing stops that from happening in terms of request and response bodies either.

The fact that existing tooling relies on some of these conventions is probably a good enough reason to do things this way, but it's not obvious to me that this is because it's actually better rather than inertia. Conventions could be developed around the body of requests as well, and at least to me, it doesn't seem obvious that the amount of information conveyed at the HTTP method/response status layer was necessary to try to separate from the semantics of the request and response bodies. I'm sure that a part of that was due to HTTP supporting different content types for payloads, but nowadays it seems like quite a lot of the common alternatives to JSON APIs were designed not to even use HTTP (GraphQL, gRPC, etc.), which I'd argue is evidence that HTTP isn't necessarily being used as well for APIs as some people would like.

To make something explicit that I've been alluding to, everything I've said is about using APIs in HTTP, not HTTP in the context of viewing webpages in a browser. It really seems like a lot of the complications in HTTP are due to it trying to be sufficient for both browsers and APIs, and in my opinion this comes mostly at the expense of the latter.


It's quite unclear what's your point. HTTP APIs should have minimal status code set. Parent described it perfectly. It's simple, practical (especially from monitoring perspective) and doesn't intervenes with a service domain.

It seems you have some alternative in mind but it wasn't presented.


I don't consider what the parent comment listed as "minimal". The alternative I described is literally in my initial comment; using only 200 for APIs is "minimal".


Only 200 is detrimental for monitoring. You have to parse response body to classify response types. HTTP status codes is a cheap and already existing way to get insights into service behavior.


It's minimal if you want to integrate with anything that understands HTTP status codes.


Need an AI playground to paste error responses and fix the code.


> Each response (other than 401) includes a json blob with details

...until you discover an intermediate later strips the body of certain error codes. Like apache, which IIRC includes all 5xx and most 4xx.


Go ahead try to implement something like cross-origin requests or multipart encoded form uploads just using the body semantics you described. I’ll wait.

Also that is not a controversial take. It is at best a naive or inexperienced take.


Both of those happen in the context of web browsing rather than existing in APIs in a vaccuum; I'd argue that there's absolutely no reason why the mechanism used to request a webpage from a browser needed to be identical to the mechanism used for the webpage to perform those actions dynamically, which is pretty much my whole point: it doesn't seem obvious to me that it's useful to encode all of that information in an API that isn't also being used to serve webpages. If you are serving webpages, then it makes sense to use semantics that help with that, but I can't imagine I'm the only one who's had to deal with bikeshedding around this sort of thing in APIs that literally only are used for backends.


Multipart messages definitely happens in APIs as well, if you are handling blobs that are potentially pretty big.


There are a lot of useful network monitoring tools that can analyze HTTP response codes out of the box. They can't do this for your custom application error format. You don't have to go crazy with it, but supporting at least 200/400/500 makes it so much easier to monitor the health of your services.


I like to find a middle ground.

I use http status codes to encode how the _request_ was handled, not necessarily the data within the request.

A 400 if you send mangled JSON, but a 200 if the request was valid but does not pass business validation rules.

Inside the 200 response is structured JSON that also has a status that is relevant at the application level.

Otherwise how can for example you tell if a 404 response is because the endpoint doesn't exist, or because the item requested at the endpoint doesn't exist?

I believe it's important to have a separation between what is happening at the API level vs Application, and this approach caters for both.


> A 400 if you send mangled JSON, but a 200 if the request was valid but does not pass business validation rules.

What about empty required field in JSON? Is it still mangled or it's already BL?


As it's not to do with the http request and the body was able to be parsed, in my book that'd be classified as being at the application level, so results in a 200 status with a JSON response detailing the issue

200 OK {status: "failed", errors: ["field X is required"]}

How you deal with this on the application side, what JSON statuses you have etc is up to you.


It's an client error and it's highly beneficial to make it 400 for monitoring purposes. You want to see your FE or mobile devs deployed a faulty app.


That depends on how you set up and do your monitoring. Not every failure needs to be indicated by an HTTP status code.

For example, on a server I'm working on there are helper functions that generate different types of responses. Responding in certain ways will produce a 200, but will also log a warning or error.

On the client side, you can create request helpers that all requests go through and that can resolve requests appropriately, rendering error messages to the user etc.

The main thing is to have a well defined, consistent approach.


It's easier to do nothing (already available status codes) then to do something, isn't it? Developers are awful at following consistent approach.


One reason for using HTTP verbs is to distinguish between queries and updates, and for the latter, between idempotent and non-idempotent updates. This in turn makes it possible to do things like automatically retry queries on network errors or cache responses where it is safe to do so.


Anecdotally the color codes make life much easier when debugging a new API. You instantly see that's something is wrong. If everything is green you don't realize that something is wrong until you carefully read a uniquely structured custom response. Saves a lot of effort.


> Honestly, my controversial take is that for APIs, it would be cleaner to not use any HTTP status codes other than 200 and have all of the semantics in the body of the response.

We've been doing that for 20 years with json-rpc 1.0

    --> { "method": "echo", "params": ["Hello JSON-RPC"], "id":1}
    <-- { "result": "Hello JSON-RPC", "error": null, "id": 1}
In this context, HTTP is just the transport and HTTP errors are only transport errors.

Yes, you throw away lots of HTTP goodies with that, but there are many situations where it makes more sense than some half-assed ReSTish API. YMMV.


Your kind of describing things like thrift and other rpc servers?


Possibly. I'm not sure why it should require switching to an entirely different protocol though; my point is that making an API that only uses POST and always returns 200 is something that already works in HTTP though, and I have trouble understanding why that isn't enough for pretty much everything.


You lose some benefits of features already implemented by existing HTTP clients (caching, redirection, authorization and authentication, cross-origin protections, understanding the nature of the error to know that this request has failed and you need to try another one...).

It's is certainly not comprehensive, but it's right there and it works.

Moving to your own solution means that you have to reimplement all of this in every client.


> understanding the nature of the error to know that this request has failed and you need to try another one...

Please elaborate. In my experience, most of HTTP client libraries do not automatically retry any requests, and thank goodness for that since they don't, and can't, know whether such retries are safe or even needed.

> redirection

An example of service where, at the higher business logic level, it makes sense to force the underlying HTTP transport level to emit a 301/302 response, would be appreciated. In my experience, this stuff is usually handled in the load-balancing proxy before the actual service, so it's similar to QoS and network management stuff: the application does not care about it, it just uses TCP.


They don't retry on errors but they know it is an error. Eg. imagine a shell script using curl or wget and trying multiple URLs as a health check (eg. on different round-robin IPs). Without these "generic" HTTP tools knowing that this is a "failure", you would need to implement custom parsing for any case like this instead of relying on the defined "error" and "success" behaviour.

The same holds true if you are using any programming library: there is a plethora of handlers for HTTP errors.

As for redirection, a common example is offering downloads through S3 using pre-signed URLs (you share a URL with your own domain, but after auth redirect to a pre-signed S3 URL for direct download or upload).


You are thinking like a developer, but there is a world of networking as well. Between your client and server will be various bits of hardware that cannot speak the language you invent. 200, 401, 500 — these are not for the use of the application developer — but rather the infrastructure engineer.


You need some kind of structured way to describe the action to take, what the result is or what the error is. so the client and server can actually parse the data. that's the protocol, whether its something formal like rpc libraries, or "REST"-ish or w/e

json-rpc is probably what your describing over http, maybe if you squint enough graphql too


Something being "enough" doesn't mean it's optimal. There's a huge stack of tools that speak HTTP semantics out of the box; including the user agent, i.e. the browser (and others), but also stuff like monitoring tools, proxies, CORS, automation tools, web scrapers...

You don't need to reinvent HTTP semantics when HTTP is already there, standard, doing the right thing, compatible with millions of programs all across the stack, out of the box.

HTTP is so well designed it almost makes me angry when people try to sidestep it and inevitably end up causing pain in the future due to some subtle semantic detail that HTTP does right and they didn't even think to reimplement.

And the only solution to such issues (as they arise, and they will) is to slowly reimplement HTTP across the whole stack: oh, you need to monitor your internal server errors? Now you have to configure your monitoring tool (or create your own) to inspect all your response bodies (no matter how huge) and parse their JSON (no matter how irrelevant) instead of just monitoring the status code in the response header and easily ignore the expensive body parsing.

Even worse when people go all the way. If we don't need status codes, why do we need URLs at all? Just POST everything to /api/rpc with an `operation` payload. Congrats, none of your monitoring tools can easily calculate request rates by operation without some application-specific configuration (I wish this was a made up scenario).

Just use HTTP ffs. You'd need a very good reason not to use it.


Yeah, that's usually the pragmatic thing to do. Facebook does that with their API, for example.

4xx or 5xx gets you the default HTTP handling for that kind of error. Occasionally - especially in small examples - that default handling does what you want and saves you duplicating a lot of work. More often it gets in your way.

I'd compare it to browser default styling - in small examples it sounds useful, but in a decent-sized site you just end up having to do a "CSS reset" to get it out of the way before you do your styling.


This is the way to go, pretty much solves, 404 resource not found or route not found. But you will get laughed at by so called architectural dogmatists. Remember we aren't really doing REST, it's just RPC and let's call it that.

Shoehorning http protocols error codes as application error codes, drinking the cool aid and calling it best practice is beyond bizzare.


Agree. "200 - successfully failed to do the thing" is valid and useful.

500 is "failed to do anything at all"




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: