It's no news that REST, in essence, is focused around the concept of
resources. In some cases, it's more intuitive to create a mental mapping
between a business item and a "RESTful" resource which makes API
development a breathe. For example, a photo can be viewed as a resource
on Instagram, a document in Google Docs, a user in a social network, a
tweet on X, and so on. However, there are other situations where the
parallel might not be so straightforward. That type of scenarios usually
fall under "processing" categories or "actions": "pay", "translate",
"trade", "encrypt/decrypt", "encode/decode", etc.. When we operate in
these terms we might be tempted to lean towards a non-RESTful style
where an endpoint starts to represent a "method or function" instead of
a resource (e.g. /pay
, /encrypt
). This is not
a wrong solution and simply called RPC
(Remote Procedure
Call) and is a great alternative to REST (more on that later). Since
it's best to avoid a mixture of multiple concepts such as "resources"
and "actions" within one API, let's take a look at how we can "re-think"
or "fit" that type of functionality into existing REST API.
Solutions
I would like to start with /pay
example:
Convert verbs into nouns. That is probably one of the most natural strategies, therefore, is used more widely:
POST /payments Host: payments-api.com { "amount": 100.00 "currency": "USD" ... }
Here
/pay
endpoint is substituted with/payments
. The operation adds a "payment" to "payments" collection. The real-world illustration of the applied method can be found in Stripe api where instead of a word "payments" they use synonym "charges".However, there are other circumstances where the current approach would be less transparent. Imagine a "hypothetical encode API". How would you represent an encoding action with a resource-based collection model?
/encoding
,/encoders
? That is where another technique comes into play.Determine the response of an action. One of the good names for the resource returned as a result of encoding operation can be
cipher
:POST /cipher Host: encoding-api.com { "data": "HelloWorld" "schema": "base64" }
The solution is aligned with REST principles around resources, but not as much with "Cacheability" and "Uniform Interface" as a result of which may not be considered truly RESTful:
Since each request containing the same combination of "data" and "schema" returns the same response it should be cachable. Even though according to latest HTTP standard POST requests can be cached (caches also can be different - "private" cache such as browser or "shared" cache such as nginx), it goes against REST principles of "idempotency": POST operations should always reach a server to create a new resource, always return a different result (with id) therefore they are not cachable from the REST point of view.
On the technical side of things cache configuration for the "body" and "URI" combination requires additional setup compare to default and a more simplistic variant with "just URI". This functionality may be not even supported in some load-balancers/proxies.
Goes against REST Uniform Model. When we POST resources in REST they are not always, but typically hold unique content (going back to our example with users in Facebook, tweet in X or photo on Instagram) with an intent that a new resource will be created. Looking at the encoding example from the business of view is nothing more than retrieving a cipher, rather than creating it.
Refactor to utilize
GET
and Content Negotiation with a customAccept-Schema
header:GET /cipher/HelloWorld Host: encoding-api.com Accept-Schema: base64
Refactor to utilize
GET
:GET /cipher/base64/HelloWorld Host: encoding-api.com
While examples 3 and 4 can probably satisfy most demanding REST
advocates they often will go against business requirements. Take a look
at our "encoding API": how should we handle string with special
characters/whitespaces (e.g. "Hello World $&")? Does the conversion
into Hello%20World%20%24%26
on the client and then back to
Hello World $&
justifies REST "purity"? More than that,
if we replace our "hypothetical encode API" with "hypothetical
encryption API" it becomes clear that solutions 3 and 4 will not be even
an option for security reasons (can't have sensitive data in the
URI).
To complete the list of all available options, I would like to mention one more, lesser-known approach:
Use
GET
request with body:GET /cipher Host: encoding-api.com { "data": "HelloWorld" "schema": "base64" }
Yes, message body is allowed in HTTP
GET
request. It also seems to be solving "conceptual idempotency" by applying more appropriate semantical operation, but this solution should be used with caution as it has following downsides:- From the technical side, cacheability setup is still not ideal as it needs to be implemented using "URI" and "body" compare to "just URI" as in example 4
- Not completely aligned with Uniform Model. A resource should be identified by its URI only (HTTP Spec) and the body should not have semantic meaning to the request (Roy Fieldings) source
GET
with the body is not widely spread combination and may not be supported by the technology of API consumer
Nonetheless, the aforesaid approach is used by a very popular search engine Elasticsearch. They do, however, provide a fallback mechanism in a form of
POST
requests which becomes identical to example 3.
Recap
We took a look at a couple of approaches that can help align action-based API's or parts of the API's to be more geared towards REST architecture. There is one very important part here that I can't stress enough: even though some of the proposed solutions have their downsides, all of them would solve a business problem in one way or another. When thinking in terms of a business domain it's important to weigh the pros and cons and take the solution that has more advantages. It depends on what kind of problem we're trying to solve - is the goal to deliver certain functionality to API consumers? Maybe it makes sense to sacrifice REST compliance and go with solution 3 or 5? On that note, I would like to point out Stripe example to show that in some cases it's possible to deviate from core REST principles by using verbs in the URL:
I also like the fact that they don't consider them fully restful, but rather "organized around REST":
That's a great wording that not only highlights the core principles but also notes that it has the freedom to go with a different solution when there is a need. On the other hand, the target business goal might be formulated as "to be fully RESTful" therefore proper choices should be made.
Finally, I would bring one more consideration to the table. At the
very beginning of this post, I used the term "fit" not by accident. If
the target API is action-based maybe we're trying to adopt a REST in a
place where it is not a good fit? Maybe it's worth skipping over REST
and consider, an alternative, HTTP-JSON-RPC style? That's the model of a
Slack's API where it utilizes
HTTP as a transport, JSON as a message format, and
METHOD_FAMILY.method
endpoint convention. Since covering
nuances and details of RPC
-style API is a different topic,
I want to share this smashingmagazing
article that has a great overview of this problem.