In the past, when tasked with building an API for a web site I would define a suite of URLs to handle the various tasks I wanted to accomplish.
- If I wanted to get a list of posts I would
- If I wanted to publish a new post as the user Sam I would
- If I wanted to edit an existing post I would
PUTthe new data to
Because that’s the right way to do it, right?
Well, I’ve thought about this a whole lot and have come up with an alternate approach that I think works quite well.
Maybe it’s a terrible idea, maybe it’s useful; it’s almost certainly been done before. Let’s jump right into it after a picture that Medium says will “capture people’s interest”.
Behold, a scenario: I’ve got a site that sells t-shirts. It’s called Wit-T-Shirt, a clue to the hilarity of the slogans on the apparel.
Somewhere on this site there’s a button that lets a user add a product to their shopping cart. In the browser, clicking this button will call a function called
addProductToCart and pass an object containing the product details and the ID of the user that did the clicking.
Then some other stuff will happen — the topic of this post.
Then eventually, on the server, a function
addProductToCart will be called which expects the user ID and the details of the product. It will then go and check stock levels, update the user details in the database, calculate postage, and whatever else.
The way things start out and the way they end up are perfectly sensible, it’s the steps in the middle that have been troubling me.
The status quo
Up until now, I would have approached this the same way I think a lot of people do.
On the client (in that
addProductToCart function) I’d split the data apart to create a URL with the user ID in it, then I’d initiate a network request with the method of
POST (after spending 10 minutes Googling if it should be a
PUT or a
POST) and stuff the remainder of the data in the body of that request.
On the server, I’d create a route to handle this request. It’s about the same in any language, but here it is in NodeJS with Express.
I’m taking the data that is now split between the URL, method and body, and joining them back together. The user ID from the URL, the product details from the body, and the fact that I want to add something to a cart is inferred from a combination of the HTTP method and the path.
Speaking of URL encoding, what a strange little thing the URL is. I mean, think about it, the path is an array of variables — a mixture of resource descriptors and IDs — converted to a string and joined with
/ characters. Oh and also if there’s a
? in there, then the part after that is an array joined by
& symbols, each of which is a key/value pair separated by an
= symbol. And all in the form of a string with a limited set of allowed characters. What a terrible vehicle for transporting information!
If only there was a better way…
Introducing, the O API
An Obvious API is an API where you say what you want. It is a dumb name (especially the space after the O) but I’m sticking with it.
Let’s look at the above scenario implemented as an O API.
Since I no longer need to chop up my information and ram it in various holes of the HTTP specification, I can use the same URL and HTTP method for all requests — they no longer convey meaning.
body of the request I’ll say explicitly what I want to do (
action) and pass the data needed to do it (
And on the server, I’ll replace my route with this:
Since I’m now sending all information in the body of a request, I can abstract all the other HTTP details into a function named something straightforward like
And now when I want to communicate with the server, I deal only in ‘actions’ and ‘data’.
(It’s up to you where you put your
response => response.json(), I’ve left them out of these code snippets because there’s no difference between the two approaches.)
You might be thinking, um, hey stupid, that’s the same amount of code.
Well, yes, you are right. But the aim of the game isn’t fewest lines. The aim of the game is code that’s easy to read, easy to write, and hard to get wrong.
So the real benefit — as I see it — is that this removes code that implies what needs to be done and the matching code that infers what needs to be done, with code that says explicitly what should be done.
Now, if I extend this logic to replace many endpoints, I can employ a handler object and call the appropriate method using bracket notation, and while I’m at it handle requests with no matching handler.
The way I actually handle the requests on the server is the same for an O API or a REST API, except with REST I have to gather up information strewn between
req.query before doing something with it.
If you have a fake-enum like so:
In the browser you would have
And on the server, if each of your handlers is its own file, you might see:
If you’re a Redux user this is probably looking pretty familiar. The way your action creators dispatch an action and a payload to be handled by the store is just the same as the way you dispatch an action and a payload to the server.
And why would they be different? In both cases you’re sending a message to a different part of the application to do something with some data. Who cares if it’s elsewhere in the browser code or back on the server?
David M in the comments had a good point that well structured REST URIs can be a good thing if you want to keep things neat and tidy. But really, any structure you can create with URLs you can create with your action names, for example:
The ‘uniform interface’ of REST can be implemented in the path of a JSON object; a
/users/cart is not so fundamentally different to and action name of
Some name dropping
It’s interesting how changing one little thing can make this seem like a whole different approach. Maybe I just need some sleep, but
fetch now resembles an event emitter,
app.post('/api' ...) feels like a listener and the body smells like a ‘data transfer object’.
So am I just describing or some butchered version of event-driven architecture or message-driven architecture, or remote procedure calls (or JSON-RPC) or any number of other things that I haven’t heard of?
Maybe, I suppose, if that’s how you want to see it; but really I’m just doing what I’ve been doing all along, with a few of the implementation details around the network request changed.
I would be remiss if I didn’t mention GraphQL. So …
If you are in the rare and unfortunate position of valuing my opinion, here it is: as you start to design your next API, work out into which of these two buckets it falls:
- The API serves your back end. You want it to allow controlled access to CRUD operations on the underlying data. It is generic and has no knowledge of the application making the requests.
- The API serves your front end. The API’s role is to serve the needs of a specific user interface. It must provide data in the most appropriate format and make it simple for the client to send instructions to the server, allowing the client get on with the business of rendering pixels and handling user interactions.
Clearly, if option 1 is what you want, a REST API is a good solution. But in Roy Fielding’s own words: “a uniform interface degrades efficiency, since information is transferred in a standardized form rather than one which is specific to an application’s needs”. So…
If you’re writing an API that will only ever be consumed by your own front end code, and you value code that isn’t more complex than it needs to be, maybe have a think about considering weighing up the option of assessing the viability of an O API.
I’d add that since REST is the norm, anything you’re exposing to the public should probably be REST (although some big APIs, like Slack’s eschew it for something a little more RPC). And you could extend this logic to say that if you’ve got 100s of developers, perhaps sticking with the most common approach has it’s benefits.
And it doesn’t need to be one or the other. If you’re a big fan for REST, think about which parts in particular you’re a fan of. If being able to identify resources in a structured way is important, you can do that without needing to spread your information between URL/method/query/body. If you rely on your URL representing a resource (for caching/routing/logging) then you could drop the action name in the URL (as Slack does). (Unless you’re caching based on query parameters, then obviously you need the query parameters and should probably be using REST.)
Whenever I write a post of this nature (a category I call “you’re all doing it wrong and I’ve come up with a new way even though I don’t really know what I’m talking about”) I tend to get quite a few negative comments.
I don’t have anything to say to quell the flow, I just want you to know I saw it coming.
Thanks a whole lot for reading, have a super day!