How to (and how not to) design REST APIs
github.com
github.com
It gives this as a ‘bad’ example:
GET /v3/application/shops/{shop_id}/listings/{listing_id}/properties
With the justification that “The {listing_id} is globally unique; there's no reason for {shop_id} to be part of the URL. “No the point of the API is that /v3/application/shops/{shop_id}/listings/{listing_id}/properties is a globally unique identifier. Your belief that parts of that id have global meaning outside the context of that identifier is irrelevant - that path is the identifier for the resource.
And having hierarchical paths is useful because you can do things like manage permissions on parts of the hierarchy - users might have permission to check listings in certain shops and we can characterize that as them having permission on /v3/application/shops/{shop_id}/listings/*.
Directory structures of resource identifiers are good and logical and not a ‘bad’ API design practice at all. You might as well argue the UNIX file system is a bad design because all the files have a unique inode id so paths are completely unnecessary.
No, the shop id is not in fact part of the globally unique identifier of an Etsy listing, and the properties are not dependent on the shop. Etsy listings have a 1:N relationship with Etsy shops.
The API was a mistake, which they are slowly correcting - they've already changed:
GET /v3/application/shops/{shop_id}/listings/{listing_id}
to: GET /v3/application/listings/{listing_id}
...and I presume they will eventually change the rest of the listing-related endpoints over time.Managing permissions using the hierarchy of a URL is silly at best, dangerous at worst. The first thing any attacker will do is plug in an alternative shop id and see if it grants access to the non-permitted listing. If permissions are attached to the shop (and for Etsy, they are) the server needs to load the listing, figure out the associated shop, and then check permissions. The client cannot be trusted to provide the correct shop id, so there's no point in asking for it.
No plugging in a shop you have permission to doesn’t work if your resources are hierarchical any more than plugging ~/passwd let’s you read /etc/passwd because you have read access to your home directory. Those are different resources and one of them exists and is locked down and the other one doesn’t exist.
Or perhaps what you call silly is just you being unaware of what you don't know. There are valid cases to handle permissions using the structure of URLs. As well, the danger you allude to comes from handling it naively. Even the hypothetical attack you suggest might be among the first thing any non-tech savvy person might think of trying.
The scenario you're describing above is simply one of dealing with redundant information in a situation where inferring the whole from the part is not detrimental (for the platform). A case can certainly be made that with that simplification, some optimization opportunities are also lost. Perhaps Etsy doesn't need them. Others might.
> The client cannot be trusted to provide the correct shop id.
The client cannot be trusted period. If I provide a signed cookie that contains a list of authorized shops and they return something else, good thing that cookie is signed. Also good thing the cookie contains the shops, no need to touch the disk if the URL doesn't match the list.
If you are designing a ‘REST API’ you have already committed to ‘purity’. If you follow this guidance you are not designing a REST API you are designing a JSON over HTTP api with parameters in the query string.
> [having shop_id in the URL] inevitably causes problems when your invariant changes down the road - say, a listing moves to a different store or can be listed in multiple stores.
Basically the choice is between having a perpetual unique URL to a listing or multiple ones, maybe valid at the same time and some of them maybe invalid in future, when a listing is removed from a shop.
A visitor with a valid unique listing id will always be able to look at the product. If there is a shop id in the URL that URL might become invalid and the visitor loses access to the product and had to search for it again, adding friction. With the global unique is the visitor will discover that the product is offered by another shop (maybe a new one from the same tenant?) which is usually not important.
Permissions for the listing could be handled by matching the shops a user has access to with the shops the listing belongs to.
REST does not mean ‘parameters in the path not in the query string’.
Suppose that we have
/shops/1/listing/1
/shops/2/listing/1
where listing 1 is an id local to those shops, they are two different listings. What happens to those URLs when those shops remove those listings? Both of them should return 404.Then the listing appears in shops 3 and 4.
/shops/3/listing/1
/shops/4/listing/1
If we have four different records in the listings table of the database there is usually no way to relate those listings, unless we inspect all the records after creates and updates looking for exact matches. So we can't redirect.Let's say that those listings 1 are globally unique ids. There is only one record for them in the database. When that listing is removed from shops 1 and 2 and later appears in shops 3 and 4, which shop do we redirect the original URL to? 3 or 4? We have only one choice and if we redirect to shop 4 the owner of shop 3 won't be happy and viceversa. We can add some reference in the JSON response that will look like 302 Location: /shop/4/listing/1 with a body including {"also_sold_by": [3]}' but again, why arbitrarily pick the main URL?
But if we have a URL like
/listings/1
we can return a reference to shops 3 and 4 in its JSON response. Everybody is happy.So you have: |shopid|listingid|itemid|
The item is global and the listing is local to add shop specific info, or even whether that shop carries that item?
Of course this might not be a valid use case or I may misunderstand then meaning of listing.
There’s no standard. Every REST API looks different. Clients have to refer to documentation anyway, so consistent URL patterns achieve nothing. People waste large amounts of time over totally inconsequential minutiae like whether to use singular or plural words in URLs.
Separating idempotent calls from non-idempotent calls is useful, but REST overcomplicates this. All that’s needed is read and write calls, yet REST has get, post, patch, put, delete…
REST is also inefficient. Clients could read the data they need in one HTTP request, but most “RESTful” APIs force clients to make many requests for the sake of what is essentially aesthetics.
Rest is simpler, but comes at the cost of a lot of nice things like automatic endpoints generation and type verification. The problem is that the heavy tooling tends to not be there or not work correctly. But this is not a win for that kind of simplicity, that's a reason to improve the protocol design.
https://news.ycombinator.com/item?id=38103310#38104983
?
Many computer users work with a canonical version of REST every day, without realizing it. Through a peculiar turn of events, the version of REST which is widely used today is often called “The Web”, and many of its users are not aware that it is basically the REST-ful architecture, defined by Roy Fielding.
There really is a REST, and these people are using it, but it is just a part of The Web they use. REST is the network architecture: hypermedia encodes the state of resources for hypermedia clients. JSON is an essential part of Single Page Applications, but useless by itself; it can only function in the context of a complete API specification. JSON is normally used in combination with SPA libraries: the whole system is basically RPC with JSON added, or JSON/RPC. All these so-called “REST-ful” APIs are really JSON/RPC.
respectfully, https://htmx.org/essays/#hypermedia-and-rest
(The effect isn't ameliorated by modifiers, e.g. "a natural hypermedia".)
an hypermedium
Usually, including in “hypermedia”, but “hypermedia” is either an adjective or a mass noun, not a countable noun of which you can have a single instance.
“a hypermedia... ” looks like you are using it as an adjective to modify a countable noun, and when there is no noun looks like you forgot the noun; “JSON is not a hypermedia unto itself...” should probably be something “JSON isn’t hypermedia unto itself... ” (I’d actually prefer “JSON, on its own, isn’t hypermedia”, but that gets behind the issue with the use of the indefinite article.
# GOOD
GET /products # get all the products
GET /products/{product_id} # get one product
# BAD
GET /product/{product_id}
But then in Rule 2: GET /shop/{shop_id}/listings # normal, expected
Shouldn't that be "/shops/{shop_id}/listings"? Or is it plural only if you can actually GET the path (i.e. there's no GET for just "/shop") and otherwise it should be singular?ie - like 'staff' or 'species' or 'aircraft'.
Then I can add suffix to those singular ie - 'staffList', 'speciesList' etc
Avoid plural nouns in English API endpoints because English is full of irregular plurals. For example:
goose -> geese child -> children index -> indices vertex -> vertexes analysis -> analyses
This makes English plurals unpredictable especially for for non-native speakers and hurts API consistency and discoverability.
Also consider that for a CRUD interface you may need the singular form anyway (POST api/student/create), and adding the plural means doubling the API route namespace.
It's cleaner and simpler to stick with singular nouns.
Why? What's wrong with api/students/create?
> Avoid plural nouns in English API endpoints because English is full of irregular plurals.
I don't buy this. I mean, yes, it's true, but how often do people really need to write these endpoints after initially writing the client code?
URLs refer to a resource that you can manipulate. What resource is /students/create referring to?
GET /students
So you can't escape the problem unless you want `GET /child` to fetch multiple children.Also, you should avoid verbs in URLs (IMHO, of course). You're adding to the students collection, so post to students:
# BAD
POST /student/create
# GOOD
POST /studentsSo in fact you’re fetching some subset of students anyway, and the size of the returned set might be one or zero depending on your query.
Given that, “GET /student” seems just as meaningful because neither the singular nor the plural can fix the ambiguity about what you’re actually getting.
GET /students?min_age=20
Alternatively, `students` might be a collection attribute on another resource: GET /classes/{class_id}/studentsLikewise, I wouldn't expect a singular to return a single object wrapped in an array, but I would always expect "/plural" (with no further qualifier in the url) to return an array, regardless of 0, 1 or more results.
Why would returning a full set be a condition of whether or not plural is ambiguous?
So POST /students/{id}/enrollment
Or POST /students is the act of enrolling a student, so the returned Location might be /students/{id}/enrollment to reflect the current state of that resource.
The other details of the student might be at URLs like /students/{id}/details, /students/{id}/results, /students/{id}/courses etc etc
If I end up having part of a "sub-resource" in the "main" resource, then I try to always have an href, otherwise you have to put all of the information.
So GET /students/{id} might return a JSON object with an embedded "enrollment" object, but that embedded object would have an href to the full enrollment resource.
If you think `GET /student` is confusing, or more importantly, structurally restrictive as an API, you can think about it as `GET /student/filter` where the "filter" may be a specific student id, or a range of ids, or other conditions such as `GET /student/top` or `GET /student/graduated` and then all students will be just the filter "all" or: `GET /student/all`.
As for `POST /student/create`... it doesn't matter. To use one of Fielding's own examples from his blog, how'd you turn a lamp on and off via REST? Would you be like `POST /lamp`? No. It's unclear WTF is happening.
No, of course you'd be like `PATCH {"light": "off"} /lamp`!
Kidding of course but it's true that REST purity does not make for intuitive APIs in complex real-world problem domains.
Where?
And it does often end up mattering, for clarity where at its use sites where you won't have the type declaration to help you out, and for having "student" available as a name in the same scope, as it's typical to pull an item out of a collection.
/student/all looks particularly icky to me. For getting a student, it seems unlikely that we'd identify one using these words, but in other domains, they may end up conflicting with another resource. Whatever you end up doing about that, it'll surely be gross.
I think plurals are better so nyah! Heh.
Also, it's totally my job to get hung up on these kinds of details. Clarity, avoiding collisions, enabling easy expansion, and especially averting future breaking changes to deal with the aforementioned matter to others.
Meanwhile, APIs, as interfaces, are forever. And the dumbest thing to do is to decide to have two names for one type in an interface, because grammar happens to have single and plural version for words. Why would you do that? APIs have no grammar, they're not sentences, they're made of identifiers that need to uniquely identify something. So stop trying to force grammar in.
In HTML, I might have a FORM that does a POST of the lamp's switch to /lamp/switch.
Because the switch is the resource that you're trying to manipulate when turning a lamp on/off, not the lamp itself.
What I've generally done in these cases is pretty similar to https://cloud.google.com/apis/design/custom_methods which also explains the problem better than I can.
I'd be interested as to how you'd solve some of these problems without an explicit verb in the path.
`GET /staff`
?
That said, I don't love your example. Staff does have a plural, staffs - as in, the separate staffs of multiple organizations.
ie:
GET /species
and
GET /species_list
?
Of all the rules, #1 one is by far the most arbitrary and least important. But it's also a thoroughly established convention. If you want to present "this is a normal, boring API with few surprises" to your clients, I wouldn't recommend odd collection suffixes.
But it's not going to fundamentally change the usability of your API, unlike many of the other rules.
GET api/student
POST api/student/create
DELETE api/student
it should be
POST api/students
GET api/students
DELETE api/students
I guess it’s in support of your point, but if you’re going to pluralise index as “indices”, why wouldn’t you use “vertices” for vertex?
* If you’re going to forbid people changing a parameter with a PUT or PATCH request, then the schema for these shouldn’t list them as parameters. This seems to creep in to APIs constantly as people are lazy and will use the same serializer method as for POST with an additional check somewhere in the code that changes the response. Just don’t do it!
* Don’t change the response format based on query parameters. It makes it hard for typed languages to use the API because the client has to handle all of the weird response types you’ve got. Inevitably you end up with more and more getting added and it any client becomes crazily complicated. 99% of the time it’s not worth the bandwidth saving - and if there’s lots of useless information that clients don’t want, it’s worth thinking about whether the API design is right in the first place.
* Stick to one mechanism for doing things. Pagination and sorting behaviour should be the same for all endpoints. The end user doesn’t care that you’re a hip microservices company where teams don’t talk to each other - if the APIs behave weirdly and inconsistently between themselves, it will be hard to use.
What I have seen is endpoints trying to corral their responses into one-size-fits-all schemas in the situation you're describing, with predictable outcomes. Lots of overhead in most situations, tricky documentation, lots of optionals.
Under that premise, I have to say that at least for generic APIs with many differing clients, the idiosyncrasies of typed-language clients would not rank too highly on my list of design considerations — not when they are in the way of simpler, easier to understand responses.
> I have to say that at least for generic APIs with many differing clients, the idiosyncrasies of typed-language clients would not rank too highly on my list of design considerations
Hey, would you like to consume an exchange format that has meaningful distinction between strings and atoms? Those come from the dynamically-typed languages area!
GET /api/object/<id>?withAdditionalMetadata=1&expandChildren=1&.....
So then the OpenAPI schema has to be something like: schema:
oneOf:
- $ref: '#/components/schemas/Object'
- $ref: '#/components/schemas/ObjectWithMetadata'
- $ref: '#/components/schemas/ObjectWithChildren'
- $ref: '#/components/schemas/ObjectWithChildrenAndMetadata
So inevitably the client ends up being quite complex to handle this.But I feel 410 instead of 404 is pretty controversial:
> There are many layers of software that can return 404 to a request
Anything in your stack can return any HTTP error code - I don't see why 404 is special.
> When calling (say) GET /things/{thing_id} for a thing that doesn't exist, the response should indicate that 1) the server understood your request, and 2) the thing wasn't found. Unfortunately, a 404 response does not guarantee #1.
The server is free to return other codes for other classes of problems. The server could return 400 for a bad request, and leave 404 for "thing wasn't found", indicating it understood the request but it wasn't found.
Also surprised not to see RFC 7807 / RFC 9457 (Problem Details) not mentioned in the "structured error format" section.
> You could use 404 but return a custom error body and demand that clients check for a correct error body. This is asking for trouble from lazy client programmers. It might or might not be "your fault" when clients see eventually inconsistent data, but the support calls they send you will be real.
Make sure that your 404 responses were always documented, then tell them to RTFM.
404 is special because it's so incredibly common. Why take the risk? There are other perfectly good error codes that - in practice - don't have this issue.
I'm surprised you don't - in my experience 404's are by far the most common response to get when you haven't wired things up correctly. Sure anything in the stack _can_ return any code and response they want, but you're still much more unlikely to come across a 410 rather than 404. If that unlikeliness saves you support calls down the line then that's pretty good.
Or even more simple: Anything other than 200 means check infrastructure docs and if you don't like the 200 check the business requirements.
GET /thing/THG123
# on success:
{"id":"THG123", "name":"thingie"}
# on failure:
{"error":"no such thing"}
Working in typed languages, this requires parsing the response, determining success or failure, then reparsing the response into the appropriate type. Annoying.Of course it's not always like that, some APIs will put both the error and data in a wrapper object and one field or the other will always be null:
{
"error": null,
"result": {"id":"THG123", "name":"thingie"}
}
This is less annoying but it's still tedious. We could eliminate the wrapper if we only had an out-of-band signal to indicate whether the client should expect a success response or an error response... like maybe an HTTP status code? I mean, it's right there, why not use it?I don't get that one; why is an object with an array property more evolution friendly than an array of objects?
- Did I get a 404 on this endpoint because the endpoint doesn't exist? Or did I get that because the object I was looking for doesn't exist? Great, I need to dig into the response body to find out, indicate that I can either get a 200 or a 404 with this endpoint, and deal with the odd case where the API returns HTML regardless of the MIME type in the Accept header if the endpoint itself is not there because "fuck you, couldn't be bothered".
- Some HTTP libraries will consider anything that's not a 1/2/3xx an error. That can be annoying to deal with.
- Optional but supported user defined identifiers, it's so frustrating to work with API that passes you back an identifier.
- String identifier (names) for resources, with some kind of type namespacing, i.e. the prefix in the author's document - Consistent set of fields (create_time, update_time, annotations, ...)
- Avoid dynamic map (this is a JSON self-inflicted wound)
Resource Oriented Design: https://google.aip.dev/121
Declarative Friendly APIs: https://google.aip.dev/128
Declarative friendly makes writing scripts, pipelines so much better because of idempotency. It also pairs very naturally with resource Oriented design.
Long Running Operations: https://google.aip.dev/151
LROs are applicable to any request that runs longer than a second or a couple of seconds. Having a unified interface can be very powerful for implementing offline task workers and pipelines.
Filtering: https://google.aip.dev/160
This one is probably controversial as it's makes implementing basic filtering quite a bit harder. I haven't quite seen the issues it's supposed to solve play out in practice but it's interesting nonetheless.
Hot take: it doesn't matter. If the user cared enough about your decisions to file a bug report or a complaint, you're doing something right. Consider that a success.
Stop trying to shoehorn a creative outlet into your day job. Pick someone else's terrible design and stick to it.
Frankly I am shocked to see an article on REST design on HN in 2023. We sort of figured this one out.
One concern I have about using these user-defined headers is that in my designs I'll typically remove the payload from the AMQP envelope and propogate just the payload to the business logic. What to do if the headers need to be included in the business logic. It seems risky to use the headers at the protocol level.
Any thoughts?
You could also place these user-defined headers in a special property of the message, e.g.:
{ // message object
"_headerTable": { … },
// actual message properties here
}
It is a common convention to use underscore-prefixed JSON properties for such meta data."You keep using that word. I do not think it means what you think it means".
I love etymology in general, but don't fall into the trap of thinking that the origin of a word or an expression is the only correct meaning.
I also disagree with the advice to always use Arrays instead of Map objects. It is very difficult to partially update Arrays in an idempotent way. When you use a Map object, you can update individual items in the object by their keys. That is why I think you should avoid Arrays in API data structures as much as possible.
let myCustomIndex = listOfResults.reduce((i, r) => {
i[r.pk] = r;
return i;
}, {});
In general we call serialization "serialization" because we put things in serial order and send that sequence over the wire. You can't send maps over the wire. A map is a structure in memory optimized for direct access and modification. While JSON maps (objects) are just sequences of keys and vals, encoded as text, not an actual hashmap or a b-tree, as we surely understand.So you may as well save the redundancy and send a list of results, then format and index it however you please locally.
The fact JSON has maps (objects) at all is a lot less useful than people realize. It's mostly useful for the purpose of denoting "that's a key" and "that's a value". But actually all the work is done after the JSON is being read. You can just as easily send a "map" like this:
["key1", "val1", "key2", "val2", ...]I realise this is pretty DynamoDB specific. But there are also other points to be made against arrays, such as being forced to maintain their order in any kind of database, which can be quite bad for performance. When using a key value map object, there is no guarantee of the order of the items and users of the API will reflect this in how they use the API, relying on the object keys instead of array indices or array order.
Adding redundancy to input/output so you don't have to make local decisions for your working representation may seem like a simplification, but you're basically chaining yourself from doing what you need to do in order to do work effectively, and burdening input/output with concerns that don't matter on the wire.
We keep seeing this idea come back again and again where "you don't need services" or "you don't need controllers" or "you don't need mapping", so just, you know, grab the database and hose it out over HTTP and into clients as-is. But this always is one of those "immediate gratification" choices that ends up biting you in the ass not long after. It looks great in slides and demos though.
Essentially we're discussing the cohesiveness vs decoupling of a transfer format and local representation. But purely on the objective side I have one strong argument: the transfer format of HTTP-based APIs is designed to be client neutral. It won't be just JS in the browser. It may be Swift on the Phone or .NET on a laptop.
And so coupling tightly transfer with a specific client can be a good choice only in the narrow scenario where you control both API, client, and there's no another client. You can always extract more of such a scenario. For example you may not use JSON at all, you can use a custom binary format that directly dumps whatever you want in your app.
But in the general scenario where the API is an API, and the client is a client, what I said stands.
An example of a null property:
{
"firstName": "John",
"middleInitial": null,
"lastName": "Doe"
}
Compared to the omission of the property: {
"firstName": "John",
"lastName": "Doe"
}
Essentially the idea discussed in this Stack Exchange post (ignoring the use of empty strings as an option): https://softwareengineering.stackexchange.com/questions/3437...Rules #1, #2, #3: I don't feel these are rules as much as aesthetic opinions. The only thing that matters is for URLs to be unique, and no client should rely on parsing slashes on URLs to derive how data is structured.
Rules #4, #5: Very specific to how data is serialised, I wouldn't take it as a rule, although the future-proofing argument is good.
Rules #6, #7: I feel these are sensible rules in general, not even specific to REST.
Rule #8: 404 or 200 is context specific, but what I would say is that if you can represent the absence in the resource do it, otherwise use 404.
Eg: if the student 99 doesn't exist, GET /students/99 returns 404; but if you want to represent there are no students, GET /students/ can return 200 OK with a [] body – since that _is_ the representation of such information. Many APIs fail here, returning 404 on a resource like GET /students/ that is expected to always exist.
And, definitely don't use 401 GONE in a way that's not in RFC.
Rule #9: I believe it's a good idea to minimize the variation in resource representation in general, this benefits both the client and the server by allowing the reuse of cached data.
Rule #11: Agree. I would go further and even ignore mechanisms like 24 hours temporary idempotency keys - just straight allow clients to PUT a resource with whatever ID, following advice from Rule #6, and be done with it.
All in all, this shows "REST" really means different things to different people at this point, we probably need better definitions for the good practices at the different levels (data structures, HTTP compliance, serialisation).
It's worth it for your public API, but it's such a huge time sink for internal APIs.
The reason is you want tools to be able to tell a difference and discern how to categorize by service (for reporting). Also, there are privacy implications for monitoring tools, since query parameters might contain sensitive data.
There already are query parameters in the URL, that is better. I wish people never went with the idea of putting parameters in the path.
Is there a better place to include the sharding key in a REST request?
If you need to use id's to be ordered, alpha-numeric ordering can be a problem. "1", "2", ... "11", and "11" comes before "2". A simple problem to fix, either by prefixing zeros or type cast to a number. It was a lingering bug at my workplace, oddly not to fix, but to communicate across teams.
Havn't we talked about building REST APIs enough yet?
https://github.com/stickfigure/blog/wiki/GitHub%27s-wiki-mak...
The article references Stripe in a few places for examples of good designs. Guess what, they use numbers for timestamps. And not for performance reasons.
It just means someone’s working with it. Developing an integration, reading logs or a dump or a raw backup, troubleshooting something, et c. This happens plenty with any system that actually gets used, and not (necessarily) because something’s gone wrong. And timestamps have a way of making it into things like query strings, may not be someone writing your json by hand. Numeric timestamps are better for naïve automatic sorting/ordering, string is better for reading and writing.
What about booleans, like { "success": false },
you opt to convert this to
{ "success": "false" }?
Edit: Bring on the downvotes. I will die on this hill.
What is an application error? If a user tries to query a ressource they are not authorized access to, then returning a 401 is appropriate, if the resource doesn't exist then 404 is also appropriate, in theory (maybe not in practice for security reasons but whatever). Nothing wrong with that.
HTTP codes are not made only for routers, caches and proxies, HTTP was made for user agents such as browsers, HTTP is one of the foundations of REST.
and I didn't downvote you, I'm just asking a question.
But basically the overlap between the classic HTTP status codes and your API's functionality is IMO coincidence. Unless you're building a BLOB store or HTTP middleware you probably do not have enough overlap for it to be truly appropriate for your domain. HTTP is the envelope. It doesn't need to mix with your custom JSON API.
Status codes are not necessarily useful for the developer directly, as they're a second channel for the same information, but they are useful for middleware of all kinds (your argument applies equally to browsers showing errors to users, and the same counterarguments apply to programmers). For example, the HTTP library I'm using has an error_for_status function which conveniently raises a runtime error, without me having to dig into the response to do that manually. Also, if you invent more kinds of errors later, or even if you just haven't published a master table of errors where I can see it, the status code will still let me extract useful semantic information out of an error kind my code has not been written explicitly to handle.
Here is a reverse one: say you have an URL that has a slug: maybe it contains the week of the year like /weeks/38
and you delete that from the app and now you say the /weeks/38 will return 200 => so all defaul caches will cache that response. now you go back and decide ahh I actually want that week so you recreate it => well if you dont configure the in front of the api caching it will return you the previous response 200+whatever error code you had.
while in case of 404 the majority of cache services I put in front of any API will by default allow bypass of cache in case of 404 and cache response by default in case of 200.
the stickfigure style... Whaddaya call that, tech beatnik? :P Just messin with ya.
It’s like human languages: REST is whatever we make of it, regardless of what academics say.
This keeps happening and all it does it cause confusion.
It's one thing to make up new words for new concepts so you can more easily refer to them in a conversation.
It's another thing entirely to give a word a vague but similar meaning without any clear way to differentiate between the original specific and new extremely nebulous concept.
Other, like OP, think REST isn't going far enough in that HATEOAS is what "really" makes something restfull. This doesn't really work in practice as if changing the schema of some JSON response can magically change the behaviour of an application. I you want to lift all the logic to the server, I guess you can use htmx, but then you are not building an api anymore but a remote rendering engine.
https://htmx.org/essays/hypermedia-apis-vs-data-apis/
REST was coined to describe the web. It has been misapplied to JSON APIs over HTTP, and the way we got here is a funny story:
https://htmx.org/essays/how-did-rest-come-to-mean-the-opposi...
other related essays:
https://htmx.org/essays/hypermedia-clients/
https://intercoolerjs.org/2016/05/08/hatoeas-is-for-humans.h...
Rule 1 - "DO use plural nouns for collections" - is an entirely arbitrary opinion.
Rule 2 - "DON'T add unnecessary path segments" - I agree with the rule, but the examples are bad because, e.g., "/listings/{listing_id} " and "/shop/{shop_id}/listings/{listing_id}" mean two different things (or at least they should). Now, "/shop/{shop_id}/listings/{listing_id}" is a complex path, so if your API doesn't need it, then I agree, don't include it. But if it does, then it would be bad to not include it.
Rule 3 - "DON'T add .json or other extensions to the url" I mostly agree with the rule, but on the grounds of keeping things simple. Here, keeping to the standard (which means using Accept). But things like supporting a ".json" suffix are nice for cases where you want to give people (not programs) access to the different representations (should be in addition to Accept).
This justification for rule 3: "URLs are resource identifiers..." is simply not true, at least for any reasonably useful definition of identifier. A URL points at a resource, that's it.
Rule 4 is good. ("Rule #4: DON'T return arrays as top level responses") You want to keep the door open to adding metadata in the response body that will be very easy for clients to accept in a backwards-compatible manner.
Rule 5 "DON'T return map structures" doesn't really make sense. Now, you shouldn't do it just to provide a lookup index -- the id should really be the inherent id of the data -- but it's a logically valid way to structure data and your API should strive to match the logical structure of the data. Also, the arguments here are not great.. e.g., "Converting an array of objects to a map is a one-liner in most languages"... that's true, but so is the converse. The openapi example doesn't make sense either. openapi v4 could have simply added a "name" property to the object in the v3 structure, right next to the "post" property -- just like the hypothetical list-based API. I would assume openapi has other reasons for the restructure, because the map-based API doesn't force it.
Well, I'll stop there. It's not all bad, but just don't take these rules to the bank.
Wait one more: Rule 8 "DON'T use 404 to indicate not found"
Come on now, why even write something like that?
The rule is more like, don't use 404 poorly. DELETE should be idempotent (that's a good rule), which means an attempt to DELETE something that could exist but doesn't happen to exist right now, isn't an error, and should return a 2xx code. 404 response for an attempt to delete on a route that doesn't exists makes sense (well, I guess unless your API is so dynamic that routes can be created and deleted on the fly, in which case even there you'd return a success code).