The Richardson Maturity Model is a great model for explaining the basics of building an http Api. Unfortunately the indexing of its layers (0, 1, 2, 3) suggests that reaching Level 1 is already REST. That is a problem because it closes people to the decoupling and integration merits that actual REST has and leaves them with an API that can only be described as JORM (Json Object-Relational Mapping): Their API is not designed to solve a problem but to get data from A to B, so the problem can be solved at B.
For Service B to solve the problem it needs to make decisions based on state. That means we have to transfer a lot of state and the resulting json needs a huge schema that needs to stay in sync. It also means that Service B has to interpret that data in the same way that Service A would, which means code duplication between different services and often different teams, which leads to coupled releases and coupled teams. This form of coupling is inherent with RPC (remote procedure call)-style Apis and it is something that REST was designed to solve.
With this in mind I present the updated model, Richard’s Maturity Model (sorry for the name, couldn’t resist):
When you think REST, think Hypermedia. The inventor of REST, Roy Fielding, described the architectural style quite well in his dissertation in 2000, and explained the point again in 2008 (REST APIs must be hypertext-driven).
An example
To make this more transparent, let’s see how JORM and REST differ when we model a bank Api for viewing your account balance and transferring money.
JORM-Style
{
"id": "aa-11",
"ownerId": "b2-d2",
"balance": 490.00,
"currency": "EUR",
"transactions": [
{
"id": "aa-11-11",
"source": "b2-d2",
"target": "c3-pd",
"amount": 10.00,
"currency": "EUR",
"date": "2024-08-20T01:23:45.678Z"
}
]
}
It’s possible the Api is too simplistic but it’s a good place to start discussing things because this Api is missing some important things:
-
Where can we find further information? Where can we lookup the source and target of the transaction?
-
What actions are we allowed to take? Can we transfer more money? Can we close the account?
The first question is usually addressed by something like Api Blueprint or OpenApi which provides documentation for a human to sift through, guess and finally try if GET /api/users/b2-d2
or GET /api/owners/b2-d2
provides the right data for the source of the transaction.
It would be more convenient and less error-prone if the Account-Representation would tell us where the data is.
It would also make our Api evolvable.
The JORM approach leads to code where URLs are constructed via string concatenation, which means the URLs cannot change.
The URL is in fact the Api.
If we instead not hard-code anything but just follow links that the server supplies then the server can change them at any time.
The second question cannot be addressed by JORM in a satisfying way because it always requires code duplication: If we can and are allowed to withdraw money or close this account is dependent on multiple business rules. A transaction is always possible unless the account is closed and unless the balance is below zero, but sometimes below zero is also ok but only up to a certain balance and so on. A client would have to implement the same rules as the server and always keep them in sync lest it allows something that the server rejects.
REST-Style
REST solves these problems, because REST publishes all the possible actions from the current state. A client can simply look at the list of actions and decide what to do next. It does not need to know nor care why an action is allowed and thus less information needs to be shared and no duplication occurs:
{
"ownerId": "b2-d2",
"balance": 490.00,
"transactions": [
{
"source": "b2-d2",
"target": "c3-pd",
"amount": 10.00,
"date": "2024-08-20T01:23:45.678Z",
"@controls": {
"self": { "href": "https://cool-bank.com/api/transactions/aa-11-11" },
"coolb:view-source": { "href": "https://cool-bank.com/api/transactions/b2-d2" },
"coolb:view-target": { "href": "https://cool-bank.com/api/transactions/c3-pd" }
}
}
],
"@controls": {
"self": { "href": "https://cool-bank.com/api/accounts/aa-11" },
"coolb:close-account": {
"title": "Close account",
"href": "https://cool-bank.com/api/accounts/aa-11",
"method": "DELETE"
}
}
}
Additional hypermedia elements | |
Link to self | |
Link to source and target of the transaction | |
The presence of this control means that we are allowed to close the account |
The response is missing the control for transferring money from a source to a target. It is missing because the account owner is on vacation right now and does not want any transfers. Or maybe it is missing because the account is closed.
We don’t actually need to know the business reason. All our client sees is that the control is missing and therefore it cannot do it. We don’t need to duplicate why something is not allowed and that means Service A is free to add or remove rules whenever it wants and the client, Service B, just needs to react to the presence of a control or its absence.
This is a supremely powerful concept and why the relatively new REST-style from the year 2000 is better at decoupling teams than the older RPC-Style Apis. The cost is that the services require more protocol knowledge as Oliver Drotbohm describes. If we adapt his drawing so it mentions JORM and REST we get:
Smart Server, Dumb Client
The client only having protocol knowledge gives us very clear benefits when we design a SPAs (Single-Page Applications) or need to support multiple devices such as iOS or Android.
A priori the client just needs to know the relevant relations, such as "coolb:close-account"
.
The prefix coolb
just means that it is a relation of the domain cool bank.
There are multiple standardized relations that the client can use such as next
or prev
for pagination, self
for the resource itself or up
for the parent of a resource.
Once a client connects it just need to look for the existence of the "coolb:close-account"
or the "coolb:transfer-money"
to know it should display the buttons for closing or transferring money.
It ignores controls and optional fields that are unfamiliar or renders generic widgets based on the inputs the server requires.
All in all a lot less code is required for each client, because the server controls the interaction and you have a lot less errors to deal with because your iOS, Android or Web Apps cannot mismatch state.
If that seems far-fetched then please consider that you are viewing this post in your browser that allows you to react to arbitrary content without Mozilla, Google, Apple or Microsoft making adaptions in their client.
All your browser has is the protocol knowledge and that is enough. If we design our Apis the same way the web is designed, then we get the same decoupling and scalability benefits that have powered the last 35 years (the web went public in 1991). This is not surprising because Roy Fielding’s dissertation, which defined the REST architectural style, was defining the core properties of HTTP so he knew which changes would benefit the protocol.
What makes HTTP significantly different from RPC is that the requests are directed to resources using a generic interface with standard semantics that can be interpreted by intermediaries almost as well as by the machines that originate services.
http://mamund.com/blog/archives/1107
The Why of JORM
By now it should be clear why REST has clear benefits and why a small but vocal group (so called RESTafarians) take every opportunity to point out that most street-REST is not REST (REST: I don’t Think it Means What You Think it Does).
It seems that this group has switched to using the name Hypermedia and Hypermedia Apis when talking about actual REST though. Semantic diffusion has taken its toll but since the REST ideas are so great and there is a dissertation describing them, it is worth bringing back the term REST. For that to work we need a name for the alternative, and since that alternative is close enough to what many object-relational modelling tools give us, the name for the alternative is simply JORM.
The name encourages discussions.
As soon as you say, "Should we build a JORM or a REST Api?"
, people get curious.
Curiosity alone might be enough to get people to research and spread great concepts like Hypermedia controls.
Should everyone adopt REST?
Now for the real question: should everyone adopt REST over JORM?
Teams that own their clients
I think the sweet spot is when teams own their server (backend) as well as one or more clients (frontend: SPA, iOS or Android app). When they adopt REST, they can make their clients significantly dumber and by that not only save a lot of code but also iterate faster because their client is decoupled from their server.
Teams that don’t own their clients
It is less clear for company-wide or even global Apis if REST is a good choice for two reasons:
REST Apis make it very easy to solve problems because the server exposes all allowed actions in the form of Hypermedia Controls. That however means you have to somehow figure out the problems your clients want to solve. And then you have to take the time to implement solutions to your clients problems.
If you just expose a lot of state to their outside world, you don’t have to invest that time. You do have to support that exposed state for a long time though, lest you lose your clients again, and that might take more time in the long run.
The second problem is that most clients are not used to the discoverable nature of REST Apis and demand stable URLs. Even without documentation teams often try to retro-engineer the URLs and when they inevitably change, their clients break, which is what Hyrum’s Law describes:
With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.
https://www.laws-of-software.com/laws/hyrum/
That however means the Apis cannot evolve as intended which is why solutions like FIT URLs are proposed where all your URLs are signed and if you don’t have the right signature for the URL, you get a 404 Not Found
, even though the URL exists.
It forces people to follow links and it’s worth looking into, to keep your Api evolvable, but why do I have to force people to use REST?
Irony
irony, noun
the use of words to express something other than and especially the opposite of the literal meaning […]
https://www.merriam-webster.com/dictionary/irony
REST powers the internet and has allowed it to scale for the last 35 years. It should be perfectly suitable for company or global Apis. But after freeing ourselves from RPC-SOAP, we unintentionally conditioned ourselves to like RPC-JORM but call it REST. Whenever we try to rethink our approach, we still go for RPC (grpc) and sometimes even drop designing solutions altogether to instead offload the problem solving to the client (GraphQL). It’s not that these tools are bad, depending on the use case they might even be a good choice. It is however strange to compare REST, an architectural style, with a tool, specifically one that implements the RPC architectural style (some "comparisons" are very good at explaining the difference "A no nonsense GraphQL and REST comparison" by Phil Sturgeon).
Also ironic is that people "in the know" perceive building a REST Api as too difficult when really it’s very easy to get started. I’d like to change this perception and have prepared a small Getting Started.