This week I’ve been thinking a lot about APIs…
I’m building a wiki that consists of a client — which is a single page application running in the browser — and an API running somewhere in the cloud. One of the many types of interaction between them involves the client fetching a wiki page from the server via some call such as this:
const page = fetch(`/pages/${pageId}`, headers)
The questions I’ve been asking myself are:
How does the client know what server resource path to use?
How could the server change the route on which it serves pages, without breaking any clients?
Clearly there is coupling between the client and the routing within the server. And this kind of coupling exists whenever two processes communicate with each other via HTTP.
This is, of course, analogous to any function call: the caller needs to know the (fully qualified) name of the function to call, together with other details of its signature — input parameters, return value(s), side effects, etc. But in the HTTP case, there’s no compiler or linker that can resolve the address for me at development time — so if either party in the contract changes, I only find out at run time. These factors mean that discrepancies here are costly to detect and to rectify (this is the distance or reach attribute of connascence biting us).
Is the coupling implicit or explicit?
And if it is implicit what (if anything) can I do to make it explicit?
What would you do here? Would you fix it or let it go?
Thinking about APIs
I guess HATEOAS is an attempt to partially address this, but still relies on knowing (at a minimum) the root resource path, and (likely) the 'key' (rel in this example) needed to navigate to other resources.
{
"id": "10",
"link": {
"rel": "designation",
"href": "/demoApp/employees/6/designations"
}
}
Presumably strongly typed clients (generated as part of the CI/CD process for the API) offer some mitigation, but I wonder if that's just hiding the coupling. Then there's the practical issue of requiring the developer (or some automated process) keep up to date with new versions of the auto-generated API client.
@Jon is right in that (REST through) HATEOAS solves this issue. Yes, you do need to know the root. Once you know the root you can negotiate the format that the response will have - so you get to Hydra, HAL, Siren, etc. The client and the server will always need to have a common understanding about the structure of the messages exchanged. The essential information is then discoverable by navigating the structure.
The same client can talk to multiple services without the need to know exactly what the meaning of the information is. The meaning is described and derived from the structure.
All these formats include ways to _evolve_ your API. This means that old clients will be parsing the old structure, and new clients will be able to do more with new fields from the structs. There is no direct coupling between them and there is no need to keep them in sync. The server and the client can evolve on their own.
(There a huge discussion around versioning, whether it is needed at all, whether they are part of URLs, media types, etc, but I'm going to skip that. In theory, a well designed API can just evolve.)
Having said this, there is a tradeoff to the experience the client can present to its users. Due to the generality of the meaning it can understand, it is not possible to present information in the most optimal way. This becomes more apparent when building complex and highly interactive apps..
We could go on forever about what exists on this space (and it is truly amazing), but I think this blog is focused more on coupling within a local unit of code (at least that's how I perceive it and I may be wrong). The same technics that are used to fix the coupling between a client and a server across the network can be applied to fix the coupling between a function and its caller. Clojure has created a culture around this. Rich Hickey's talk on spec (Spec-ulation) is about this. From his - very interesting - observations, tooling has been build to make such issues discoverable and preventable.