Implementing Microsoft REST API Filter
sergeykibish.com
sergeykibish.com
https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypert...
> I am getting frustrated by the number of people calling any HTTP-based interface a REST API. Today’s example is the SocialSite REST API. That is RPC. It screams RPC. There is so much coupling on display that it should be given an X rating.
> What needs to be done to make the REST architectural style clear on the notion that hypertext is a constraint?
The Web (the prototypical REST application) had a lot of good ideas, not just hypermedia. It certainly makes sense to attribute those good ideas to REST.
See also: Agile. DevOps.
It's pretty common once you look.
Rest pedants don’t care if you don’t use rest, as long as you don’t call it rest. It’s hardly a difficult concept.
E.g., in Azure, which is also a "RESTful" API that has no idea what REST is about, MS completely misses Fielding points that most of the effort of definition should be spent defining the content / data's format, not things like URL structure. That way we can speak about MIME types / content-types, and know what structure we're describing. But Azure will happily describe in JSONSchema a single type, and declare that it is used for both PUT/GET, and … it's not. And discovering the additional constraints that exist on the type in the PUT is gleaned only through calling the API, certainly not through Azure's docs. And that's assuming you get a usable error in response.
JSONSchema is also a bit of a disappointment. On the one hand — yay, a spec? But on the other hand, it fails to capture so, so much. Half the fields in the type will be required … and the schema will say they're optional. Sum types of any kind are particularly badly handled, and half the time are just "string" though I think this is more of a failing on MS/Azure than JSONSchema, for simple string-like enums; but more complicated sum types, IDK if JSONSchema can't cut it or if MS just doesn't get it or what. For example, to instantiate a VM, the request body looks something like:
body: required struct {
properties: optional struct {
storageProfile: optional struct {
imageReference: optional struct {
communityGalleryImageId: optional string,
exactVersion: optional string,
id: optional string,
offer: optional string,
publisher: optional string,
sharedGalleryImageId: optional string,
sku: optional string,
version: optional string,
}
osDisk: optional struct {
createOption: optional enum { "Attach", "Empty", "FromImage" }
image: optional struct {
uri: optional string,
}
managedDisk: optionalStruct {
id: optional string,
// omitted fields
}
vhd: optional struct {
uri: optional string,
}
}
// omitted fields
}
// omitted fields
}
// omitted fields
}
I've listed only the fields used in determining where to source the VM's OS disk from. And it's nuts! "properties" and "osDisk" are actually required; if you specify "imageReference" or "image" or probably "vhd" (but I've never used that myself), "createOption" must be "FromImage", if you specify "managedDisk" it must be "Attach", and the docs don't describe what meaning "Empty" has. You can specify only one of those, because otherwise, you're saying to source the image from two things which would be nonsense (but is permitted by schema/docs?)."imageReference" itself is really a sum type; you must specify (offer, publisher, sku, version[, exactVersion]), or communityGalleryImageId, or sharedGalleryImageId. You could image it being,
enum ImageReference {
FromMarketplace { offer: String, publisher: String, sku: String, version: String, exactVersion: Option<String> },
SharedGallery(String),
CommunityGallery(String),
}
And we've not even touched VHDs, managed disks, or VM images yet! And you don't need createOption.I think, again, I'm going off what I've learned the hard way about how Azure works. I shudder to think what the validation logic looks like. (I'm also reading the docs. Reading JSONSchema is … painful to start with, but Azure's schema's directory layout structure makes it triply painful.)
But even that sum type is to miss the point of REST entirely. The RESTful definition would be:
image: URI-reference
and that's it. The Content-Type of the content at the provided URI provides the type of image that it is.Oh and while I'm here: don't choose a boneheaded page size if you paginate an API call. Half of Azure's services will trickle-feed you 100 records at a time, and so the response body is like 60 KiB. Since the payload also has the next page's URI, your calls get decimated by latency. Some bad offenders: listing images in a repo in ACR gets ~ a phone modems worth of overall throughput. It takes minutes to download single-digit megabytes of image metadata. The Azure pricing APIs are similar: it's ~58KiB per page. The entire VM pricing data is something like 131 MiB, and that requires 2,235 HTTP calls to fetch.
Right there. Because typing 20 lines of source code should not be an argument for weakening your model layer. Re-use would be great if types were identical between actions, but in the real-world they're not and they accrue subtle differences over time.
I am not working on this specific API, so I am not going to comment on anyway. I hear your complains about Microsoft API guidelines (which is an entire different conversation) but I wanted to add my two cents with regards to JSON Schema.
The problem that I have been having with JSON Schema since forever - is that the data that is being modeled is complected with contextuality of its usage. For instance, if I have
type user = { name: string, surname: string, password: string }
IN JSON Schema it is very hard to give contextuality on it, and most of the times involves having two separate types.
Here is an example:
If I am creating a new user, then name, surname are mandatory, while password is not because the system is autogenerating it. If I a logging in - then I want ALL of the fields.
As of today, it is very hard in JSON Schema to express this.
Basically speaking, I am arguing that the data structure is a thing, another one is its usage in a context, where there can be requirements and complicated validation logic involving even other fields
In my experience, the only thing that has been very very close to what I have been looking for when modelling systems is Clojure. Most of the people laugh to my face when I say that primarily because it is a LISP 2 and yet... In particular, spec (and even better spec2) have the tooling to express data structure as sophisticated as we want without a type system and with the contextuality constraints that are fundamental for a real type reuse.
But even so, here the problem is that the APIs aren't actual PUT/GETs: they payload types aren't the same going up as they are coming down. It is really two separate types, one for PUT, one for GET.
Some of that is to be expected (there will be some information after the create that is only added by the VM coming into being) but how Kubernetes handles this with a separate "status" for the item I think ends up letting the rest of the type (spec, in k8s's case) be the same type. (… ish. K8s has variants of this problem, too.)
To expand a bit, I'm largely relegated to the API docs themselves. Browsing the actual schema is hard:
Start at: https://github.com/Azure/azure-rest-api-specs
Descend into specification.
Descend into … so many choices … compute.
Descend into resource-manager.
Descend into Microsoft.Compute
Descend into stable
Descend into — and this is tricky!
the latest version isn't the latest version.
The latest version is 2022-04-04, but for VM creation it's 2022-03-01.
The only way I know to determine this is to seek backwards, or find it in the docs.
Descend into ComputeRP
Descend into virtualMachine.json.
And it's 3.3k LoC! Some of this verbosity is JSONSchema, to be sure… but still. And then you might have to wade back up to common.json, though I forget what circumstances cause me to need to look there.I think ideally you want seperate structures but you need tooling which helps you map between output/input structure automatically (in strongly typed languages, it’s easy in Python or JavaScript) and that’s just lacking currently.
Don't think I've literally ever seen this done in practice. Don't think I've ever seen a "REST client" library that even offers the possibility of negotiating with a backend that's attempting to do this — let alone doing it in a streamlined manner.
https://htmx.org/essays/hateoas/
any idiot (such as myself) who has ever made a web 1.0 app has created a better REST API than 99.9% of all REST API engineers, so called
The biggest problem with the bad style of RPC-style programming was the assumption that remote calls could be treated like local calls. Modern HTTP-based RPC APIs don't make that assumption and thr problems of distributed computing are handled explicitly.
The semantics of HTTP verbs, semantically meaningful namespace trees for resources in the URL, standard authentication and encoding, etc are all nice benefits of HTTP APIs even without hypermedia.
That said, I haven't given up on hypermedia APIs! I liken them to "fluent" APIs of OOP. It's a powerful design approach, but I just wonder how to capture the benefits of that approach over what most people are doing today.
However, I would add that I think you can build machine APIs using hypermedia, but the API contract would different. You would essentially define an OOP API with MIME Types as class definitions. That is partly I think why Toy Fielding put so much emphasis on defining those types.
But the real pwer of hypermedia - the ability to change those hypermedia relations dynamically - is really only useful for human-driven interactions.
REpresentational State Transfer is, like it sounds, a very specific concept: it's about how the semantics of HTTP verbs + headers (e.g. Cache-Control headers, Allow headers, Vary headers, etc.) interact with clients and gateways (esp. caching gateways, clients in offline mode, etc); and how these interactions work better and "in harmony with" those HTTP semantics — improving cachability, decreasing number-of-origin-fetches, improving PWA offline graceful degradation, avoiding inconsistency and any need for cache-busting, etc. — when you model your exposed state-representations a certain way.
A REST model — a projection of your data as "REpresentations of State" — is a transformation of your data along lines such that each representation becomes an ADT with semantics that recapitulate those of an object store / WebDAV folder / HTML page as edited in the original WorldWideWeb.app. Each representation can be PUT; POSTed to; PATCHed, DELETEd; GET-ted and then cached; and HEAD-ed without triggering heavy computation. REST's REpresentations are objects that work in harmony with being passed around through the HTTP protocol.
However! Every "RESTful" API you've ever seen (at least if you do CRUD web-dev), is very likely a trivial RESTful API, because it's taking abstractions — resources — that are already inherently ADTs with REST-alike semantics, and just 1-to-1 exposing them via HTTP. (A lot of work may have been put in by some dev at some point in the past, into coming up with a best-practice design for those internal data structures such that they can work for the business-domain while also being inherently RESTful; but it's not REST itself carrying that weight, and it's not REST itself that determines whether it's possible to do that.)
It's great if your internal data is such that you can do that ; but that's not what makes REST powerful; it's not what it means to wield REST as a tool.
REST is a thing like GraphQL: a gateway adapter that reformulates an internal data model into a different, outward-facing data model. It's what frontend devs would call a data-binding layer — but, like GraphQL, one done on the backend. This is why REST talks about representations: representations are to REST as graphs are to GraphQL. They're a modelling layer on top of your data, that reshapes it. Except that, unlike GraphQL, which is almost as bad for HTTP semantics as SOAP, REST is (by definition) whatever transformed form allows your data to be handled most optimally in an ecosystem of HTTP clients+servers+gateways.
This concept is most powerful when applied to things that aren't inherently very RESTful at all; where it's challenging to state them in RESTful terms. For example, an API for controlling an ephemeral pushdown-automata, like an SQL connection. Or an API for managing subscriptions and consumer-groups in a message-queue broker. Or an API for scheduling jobs onto a workload manager.
Re: that last one, Kubernetes is a great example of what it means to "do REST" in a nontrivial way: it takes resources that aren't inherently RESTful, and creates abstract REpresentations of these resources (YAML manifests) that work perfectly under HTTP semantics, transferring the state of these representations to clients in a way where the client can then do things like modify the representation and PUT or PATCH it back. You can stick a caching proxy between you and a Kubernetes control-plane, and everything keeps working. You can query a Kubernetes control-plane from your browser address bar.
But note how, when you create a nontrivial abstraction like this, you end up with very custom types for your REpresentations. Because there are no inherent REST-shaped resources in your backend to manipulate; and because you want your REpresentations to be long-lived documents in their own right (in caches; on the client as something for it to hold and manipulate and patch; etc), you have to define an application-layer formal semantics for what those documents are, and do. You have to ensure that a REpresentation a client fetched a year ago, and is pushing back as a PUT today, will either work as-is; be "migrated" in a forward-compatible way when received by the backend; or be able to be cleanly rejected, by your REST server, despite that document having a schema that is now older than the REpresentation you're currently emitting from that endpoint.
How do you do that? People who don't understand REST use ugly URL hacks for this — like putting the version in the URL, the format as a file extension in the URL, etc. But these hacks fight against the semantics of HTTP.
Say you have an internal Foo ADT, that you first exposed as a FooV1 representation, and then later additionally as a FooV2 representation. If you do that by exposing e.g. /foos_v1/1 and then /foos_v2/1; then a PATCH to /foos_v1/1 can't possibly inherently invalidate a reverse-proxy cached representation for /foos_v2/1. But if you've just got /foos/1, that Vary's on Allow? Then it all "just works."
To make that work, FooV1 and FooV2 have to send separate Content-Types.
QED? ;)
Here is an article I wrote on it:
https://htmx.org/essays/hateoas/
RPC-style APIs require shared knowledge about a given end point: what arguments it expects, what data it returns, etc. Unfortunately, for historical reasons, we've come to call all HTTP JSON APIs "REST APIs". It's actually pretty funny.
Instead, REST should be used in its original context: hypermedia clients (browsers) exchanging hypermedia (HTML) with the server. I have written an alternative front end library that takes this exact approach:
Some more essays on the topic:
Because you're right, REST is a specific thing, but we do need some name for what everyone is talking about, and just saying "RPC" is much too vague.
REmote Structured Transactions. Problem solved.
If a teacher has 99 failing students, blame the teacher.
If the teacher has no students, blame the topic.
People just don't want HATEOAS as much as some would like.
I used ANTLR4 to generate a .NET tree walker that would build up an IQueryProvider expression. IQueryProvider would then compile to Expression<T> and self-optimise (simplifying boolean algebra and removing redundant expressions). We hooked this up to npgsql and viola - a sane, typesafe, query string DSL without a single line of SQL.
Nowadays I hack together Terraform and Python ML- I miss .NET dearly... it was a simpler time.