I Hate CRUD API's
With the proliferation of RESTful APIs, it is common to build UI backends with the typical Create/Update/Delete patterns, or CRUD. These end up pushing JSON data structures back and forth. While this is easy and quick to get started, the end result is an inflexible interface. Easy versus Simple is a topic on its own that should be explored.
It’s easiest to understand with an example.
Example
Let’s imagine we have a Customer
entity in our system. A very simple represntation of the customer might be like this:
{
"name": "Bob",
"address": {
"street": "100 Foo St.",
"city": "Plano",
"state": "TX",
"zip": "75023"
}
}
Very straightforward. Our API probably has a POST
operation taking the above body to create a new customer which returns an id
.
Now let’s consider changing the customer’s address. The easiest thing to do would be to add a PUT
endpoint with the following body.
{
"id": 12345,
"name": "Bob",
"address": {
"street": "202 Bar St.",
"city": "Allen",
"state": "TX",
"zip": "75013"
}
}
This feels great, we have easily added a way to update a customer address. But there are some problems that we haven’t considered.
- What exactly are we allowed to change? Can we change name? What about ID?
- How does the API know what has changed? It seems like it has to do a diff.
- What happens if not all fields are included?
- What if we now have to support international and the address structure needs to change?
- What is the business intent with the operation?
- What if I decide I want a log of each change? Do I have to record full bodies for all changes?
- What if we determined customers have multiple addresses and we rename
address
toaddresses
? - What if something changes that we didn’t even think of?!
We have quickly built our update ability, and in the process we have taken on a host of new problems that are not immediately apparent. And one hard and fast rule about APIs is that breaking changes are extremely painful. Once an endpoint is added, it is almost impossible to take away unless both client and server are easily changed.
Even in such a situation, we should always strive to adhere to good API design principles.
- The API should be resilient to change.
- An API should only “accrete”, meaning we only ADD to the API, not take away or change.
- The API should provide components with which to build more complex behavior.
- Follow the Principle of Least Astonishment.
Another Way
There is an important concept in software development uncomfortably called CQRS, which stands for Command Query Responsiblity Segregation.
The main idea of CQRS is to decouple queries from commands or processes. Or to put another way, decouple your GET
s from your POST
s. Notice in our naive implementation above that the body of our customer entity is the same across the Create and Update.
So what does a solution in CQRS look like? Well our GET
could very well stay the same, but instead of a PUT
to the customer, maybe we have a POST
to http://localhost/customers/12345/add_address
. We define our Address Change command like so:
{
"type": "home",
"street": "202 Bar St.",
"city": "Allen",
"state": "TX",
"zip": "75013"
}
This is a small but powerful shift in perspective. We are explicitly telling the API what our intention is, we want to change the address. As a consumer, we cannot predict all that a backend might care about when it comes to changing an address. But we are now providing a clear indication about what we’re doing.
Let’s revisit those questions from above.
-
What exactly are we allowed to change? Can we change name? What about ID?
- It is now clear what we can do by consulting the commands available to us.
-
How does the API know what has changed? It seems like it has to do a diff.
- No ambiguity now, the API knows exactly what changed.
-
What happens if not all fields are included?
- Not a problem anymore.
-
What if we now have to support international and the address structure needs to change?
- The structure can change more easily now. By separating queries from commands, translations are easier.
-
What is the business intent with the operation?
- Again, no ambiguity now. The API knows the intent via the command.
-
What if I decide I want a log of each change? Do I have to record full bodies for all changes?
- This is now trivial, each command becomes an entry in the log.
-
What if we determined customers have multiple addresses and we rename
address
toaddresses
?- Notice we named the command
add_address
? We only need change the query side to return multiple addresses when clients can handle it. Further, if we includeremove_address
we could even backfill addresses.
- Notice we named the command
-
What if something changes that we didn’t even think of?!
- Not a problem now, possible changes are explicit based on the commands we choose to support.
Downsides
No doubt there are pluses and minuses to all choices, and this is no different.
- Simple is not easy. Clients will need to take into account the CQRS style from the beginning. It is most likely more work for a client, although this isn’t definitive.
- Retrofitting is nearly impossible. If you are already CRUD based, a transition would be very hard.
- Slower to start. With a naive CRUD implementation, it’s very quick to get up and running. A CQRS style will require much more thought up front. For example, the
add_address
command was originallychange_address
until I realizedadd_address
was a better choice.