Let's Stop Building APIs Around a Network Hack
JSON:API has been one of the most popular standards for API development for a while now. It was conceived in 2013, battled through some rather different RC versions (changing drastically as it went), finally stabilizing with v1.0 back in mid-2015.
My interest in JSON:API has changed substantially over the years. Initially I avoided it like the plague, later I recommended it, and now I generally suggest folks skip it if at all possible.
Besides trying to standardize how you represent items, collections, meta data and errors (an excellent goal!), it sets out to solve a lot of problems such as Compound Documents.
The concept of a "compound document" — in the API world at least — is the art of squashing related data into the main requested resources. This is done to reduce the number of HTTP calls a client has to make.
Clients have for a long time been very unhappy making multiple HTTP/1.1 calls, especially over mobile networks. That concern has had a noticeable impact on how we design APIs. Multiple calls is considered a point of bad design, to the point where fetching a user will include the companies they work for, and that includes the locations a company might have, and that includes something else...
That was a bit of a mess, so conventions and standards were invented to make those includes optional, and JSON:API was one of the main contenders.
Somewhere in 2013 I also released a tool called Fractal, which was based off of code I'd been running in production since early 2013. The goal there (other than serialization) was the same thing, to facilitate embedding and nesting includes optionally.
In 2013 this all made sense. Multiple calls were bad, so we had to reduce those, without assuming too much and making our requests slower than they should be. This continued to be valid for a long time, even in 2015 when JSON:API was finalized.
Guess what else was released in May 2015? RFC 7540, otherwise known as HTTP/2. In retrospect this seems highly poetic, as HTTP/2 kinda makes the compound document aspect of JSON:API a little bit pointless, and compound documents to me go hand in hand with what JSON:API is as a standard.
Not to just pick on JSON:API, the same can be said about HAL, OData, and it also makes Fractal's nesting/embedding feel silly, like it should be ripped out in a future version.
There are plenty of resources out there explaining the differences between HTTP/1.1 and HTTP/2, so I'll let them cover it.
The main point here is that HTTP/1.0 forces you to handle DNS, HTTP handshakes, SSL negotiation, etc., on every single call, and if your homepage wants 10 things from the API then it's doing all of that junk 10 times... HTTP/1.1 actually solved this with Connection: keep-alive, but not many people seem to use that. When we enabled it at WeWork it sped up HTTP interactions by around ~10%, which was nice.
HTTP/2 not only maintains a single connection per domain for reuse, but these calls can be handled asynchronously. For a blog website, you could requests articles, and upon receiving those ask for the author information. Further callbacks may ask for comments and then comment author information.
If you can use Server Push then you don't have to wait for the articles to arrive before you can request the author information. The server will just start sending you the author information!
Seriously, HTTP/2 calling is fast.
To me JSON:API has always mainly been about reducing the size of a payload response via network hacks like compound documents, but there is another strategy used. Sparse Fieldsets — an idea fairly fundamental to GraphQL also — lets clients specify the fields they're interested in, so the API can send only those fields.
Whilst sparse fieldsets can save a few kbs, there are plenty of other ways of trimming down the size of the response.
The entire mindset of "more handshakes = bad" has forced us to shove fields into the same resource to avoid making new resources, or sub-resources. Now that we don't need to be scared of it, we can stop using hacks like the partials trade-off I recently proposed, and move towards having more resources that are a bit more targeted in scope.
Invoices don't need payment status, or an array of order items, so we don't need to worry about trimming down the response at the field level, they can just be their own related resources, discoverable with hypermedia controls.
If splitting up resources sounds like a big change and you want to just squash that actual response, HTTP/2 has your back there too. Headers are compressed with HPACK, which is similar to GZIP but a little better, and it's something Cloudflare are pretty excited about.
That's not to mention that the whole protocol is binary instead of plain-text, which gets you another nice boost. If you're still sad about JSON being slow, you can use content-negotiation to add BSON or Protobuff to your REST API, which is a large part of whats getting people so excited about gRPC.
In general, once again, the potential speed benefit of skipping a bit of that JSON coming back using sparse fieldsets, seems to be lost in the idea of making your API be less variable, having more targeted resources, which are drastically more cacheable at the network level.
Everyone else can at least leverage multiplexing by slapping Cloudflare, Fastly, etc., in front of their HTTP/1.1 setup.
If your API is predominantly web-based, and older browser support is important, or you have no idea which browsers are going to be hitting it, then maybe you need to continue worrying about network hacks like compound documents.
Combining HTTP/2 with targeted, network cached endpoints (via Fastly, Varnish, Squid, etc), makes Hypermedia Controls (HATEOAS) seem waaaaaaaay less silly.
I'm not waiting for a response before I see what actions I can take next, that's slow! — Fictional Narrative Device
No longer! HTTP clients net-http2 in Ruby make asyncing up your calls nice and easy, so you can run actions in callbacks as soon as the responses come in.
Hypermedia in general seems a lot more viable when you're not terrified of the transportation layer being slow.
It also means you're not going to be scared of making a sub resource. At work
right now we have
GET /users and they want to shove
"locations" in there
(every WeWork location that a user has access to).
That would be how you'd want to do it in 2014, but in 2017 that should be
GET /locations?user_uuid=123, or who cares. It just
doesn't need to be jammed up in there, slowing everyone down, just in case
somebody needs it.
Includes, partials, etc., can all graduate to being actual brand new requests, instead of being some weird network hack because we were scared of handshake time.
Folks often say "Why would I want to waste time making a REST API do this when I could just use X."
In the same way that JSON:API feels a little silly to me now, it makes GraphQL feel substantially less useful... Anything designed around this network hack no longer feels as useful as it once was.
Well, GraphQL is the king of all glorified network hacks, and gRPC is literally just HTTP/2 + Protobuff, I'm left realizing these little niche API setups are not quite as useful as their marketing sites are making out.
If you already have a REST(ish) API, you can just start using HTTP/2 right now and get half the benefits of gRPC, and no longer care about many of the benefits of GraphQL (which are mostly available for HTTP APIs anyway).
Maybe you can remove the "ish" from your RESTish APIs by focusing more on your API as a state machine now that you're not quite as scared of making more HTTP calls.
These previous solutions were far from perfect, and I'm going to write about real-world issues I've struggled through with compound documents in the next article (done!). I don't think anyone should rush out and change everything right now, but recognize the changing environment around the web.
As the web evolves the standards evolve to meet changing requirements, and as they evolve we no longer need as many hacks to get the job done. Frontend developers aren't make CSS image sprites anymore, and JS/CSS combination is going away too.
HTTP/2 is a representation of what developers needed, and now we can use HTTP in its entirety, without needing so many network hacks to make it performant enough to get the job done.
Using JSON:API/HAL/OData is a fine way to go about building HTTP/1.1-based APIs, and it's better than inventing your own approach to compound documents, but do you really need to building HTTP/1.1-based APIs, or is it just inertia?
Get the newsletter
Pragmatic API, HTTP And REST info monthly