98

I am planning to build a RESTfull API but there are some architectural questions that are creating some problems in my head. Adding backend business logic to clients is an option that I would like to avoid since updating multiple client platforms is hard to maintain in real time when business logic can rapidly change.

Lets say we have article as a resource ( api/article ), how should we implement actions like publish, unpublish, activate or deactivate and so on but to try to keep it as simple as possible?

1) Should we use api/article/{id}/{action} since a lot of backend logic can happen there like pushing to remote locations or change of multiple properties. Probably the hardest thing here is that we need to send all article data back to API for updating and multiuser work could not be implemented. For instance editor could send 5 seconds older data and overwrite fix that some other journalist just did 2 seconds ago and there is no way that I could explain to clients this since those publishing an article is really not in any way connected to updating the content.

2) Creating new resource can also be an option, api/article-{action}/id , but then returned resource would not be article-{action} but article which I am not sure if this is proper. Also in server side code article class is handling actuall work on both resource and I'm not sure if this goes against RESTfull thinking

Any suggestions are welcomed..

Miro Svrtan
  • 1,139
  • 1
  • 8
  • 6

8 Answers8

92

I find the practices described here to be helpful:

What about actions that don't fit into the world of CRUD operations?

This is where things can get fuzzy. There are a number of approaches:

  1. Restructure the action to appear like a field of a resource. This works if the action doesn't take parameters. For example an activate action could be mapped to a boolean activated field and updated via a PATCH to the resource.
  2. Treat it like a sub-resource with RESTful principles. For example, GitHub's API lets you star a gist with PUT /gists/:id/star and unstar with DELETE /gists/:id/star.
  3. Sometimes you really have no way to map the action to a sensible RESTful structure. For example, a multi-resource search doesn't really make sense to be applied to a specific resource's endpoint. In this case, /search would make the most sense even though it isn't a resource. This is OK - just do what's right from the perspective of the API consumer and make sure it's documented clearly to avoid confusion.
Tim
  • 1,029
11

Operations resulting in major state and behavior changes on the server side like the "publish" action you describe are difficult to model explicitly in REST. A solution I often see is to drive such complex behavior implicitly through data.

Consider ordering goods through a REST API exposed by an online merchant. Ordering is a complex operation. Several products will be packaged and shipped, your account will be charged, and you'll get a receipt. You can cancel your order for a limited amount of time and there is of course a full money back guarantee which allows you to send back products for a refund.

Instead of a complex purchase operation, such an API might allow you to create a new resource, a purchase order. At the beginning you can make any modifications you wish to it: add or remove products, change shipping address, choose another payment option, or cancel your order altogether. You can do all of these because you have not bought anything yet, you are just manipulating some data on the server.

Once your purchase order is complete and your grace period passes, the server locks your order to prevent any further changes. Only at this time the complex sequence of operations starts, but you cannot control it directly, only indirectly through the data you placed previously into the purchase order.

Based on your description, "publish" could be implemented in this fashion. Instead of exposing an operation, you place a copy of the draft you've reviewed and want to publish as a new resource under /publish. This guarantees the any subsequent updates to the draft won't be published even if the publish operation itself completes hours later.

9

we need to send all article data back to API for updating and multiuser work could not be implemented. For instance editor could send 5 seconds older data and overwrite fix that some other journalist just did 2 seconds ago and there is no way that I could explain to clients this since those publishing an article is really not in any way connected to updating the content.

This sort of thing is a challenge no matter what you do, It's a very similar problem to distributed source control (mercurial, git, etc.), and the solution, spelled in HTTP/ReST, looks a bit similar.

Supposing you've got two users, Alice and Bob, both working on /articles/lunch. (for clarity, response is in bold face)

First, alice creates the article.

PUT /articles/lunch HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?

301 Moved Permanently
Location: /articles/lunch/1 

The server did not create a resource, because there was no "version" attached to the request, (assuming an identifier of /articles/{id}/{version}. To perform the creation, Alice was redirected to the url of the article/version she'll be creating. Alice's user agent will then reapply the request at the new address.

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?

201 Created

And now the article has been created. next, bob looks at the article:

GET /articles/lunch HTTP/1.1
Host: example.com
Authorization: Basic Ym9iOnBhc3N3b3Jk

301 Moved Permanently
Location: /articles/lunch/1 

Bob looks there:

GET /articles/lunch/1 HTTP/1.1
Host: example.com
Authorization: Basic Ym9iOnBhc3N3b3Jk

200 Ok
Content-Type: text/plain

Hey Bob, what do you want for lunch today?

He decides to add his own change.

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic Ym9iOnBhc3N3b3Jk

Hey Bob, what do you want for lunch today?
Does pizza sound good to you, Alice?

301 Moved Permanently
Location: /articles/lunch/2

As with Alice, Bob is redirected to where he will be creating a new version.

PUT /articles/lunch/2 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic Ym9iOnBhc3N3b3Jk

Hey Bob, what do you want for lunch today?
Does pizza sound good to you, Alice?

201 Created

Finally, Alice decides that she would like to add to her own article:

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?
I was thinking about getting Sushi.

409 Conflict
Location: /articles/lunch/3
Content-Type: text/diff

---/articles/lunch/2
+++/articles/lunch/3
@@ 1,2 1,2 @@
 Hey Bob, what do you want for lunch today?
-Does pizza sound good to you, Alice?
+I was thinking about getting Sushi.

Instead of being redirected as normal, a different status code is returned to the client, 409, which tells Alice that the version she was trying to branch from has already been branched. The new resources was created anyways (as shown by the Location header), and the differences between the two was included in the response body. Alice now knows that the request she just made needs to be merged some how.


All of this redirection is related to the semantics of PUT, which requires that new resources be created exactly where the request line asks. this could also save a request cycle using POST instead, but then the version number would have to be encoded in the request by some other magic, which seemed less obvious to me for the purposes of illustration, but would probably still be preferred in a real API to minimize request/response cycles.

3

Here's another example that deals not with documents content but more with transient state. (I find versioning -- given that, in general, each version can be a new resource -- a kind of easy problem.)

Let's say I want to expose a service running on a machine via a REST so that it can be stopped, started, restarted, and so forth.

What is the most RESTful approach here? POST /service?command=restart, for example? Or POST /service/state with a body of, say, 'running'?

It'd be nice to codify best practices here and whether REST is the right approach this type of situation.

Second, let's assume I want to drive some action from a service that doesn't affect its own state, but rather triggers a side effect. For example, a mailer service that sends out a report, built at the time of call, to a bunch of emails addresses.

GET /report might be a way of getting a copy of the report myself; but what if we want to push to the server side further actions such as emailing as I say above. Or writing to a database.

These cases dance around resource-action divide, and I see ways of handling them in a REST-oriented manner, but frankly it feels a like a bit of a hack to do so. Perhaps the key question is whether a REST API should support side-effects in general.

AMD
  • 31
  • 1
2

REST is data oriented and as such resources work best as "things" not actions. The implicit semantics of http methods; GET, PUT, DELETE, etc serve to reinforce the orientation. POST of course, is the exception.

A resource can be a mixture of data ie. article content; and metadata ie. published, locked, revision. There are many other possible ways to slice up the data, but you have to go through the what the data flow will look like first in order to determine the most optimal one (if there is one). For example it may be that revisions should be their own resource under the article as TokenMacGuy suggests.

Regarding implementation I would probably do something like what TockenMacGuy suggests. I would also add a metadata fields on article, not revision, like 'locked' and 'published'.

1

Don't think of it as directly manipulating the state of the article. Instead, you're putting in a change order requesting that the article be created.

You can model putting in a change order as creating a new change order resource (POST). There are lots of advantages. For example, you can specify a future date and time when the article should be published as part of the change order, and let the server worry about how that's implemented.

If publishing Is not an instantaneous process, you don't have to wait for it to finish before getting back to the client. You just acknowledge that the change order was created and return the change order ID. You can then use the URL corresponding to that change order to share the status of the change order.

A key insight for me was recognizing this change order metaphor is just another way to describe object-oriented programming. Instead of resources, we call then objects. Instead of change orders, we call them messages. One way to send a message from A to B in OO is to have A call a method on B. Another way to do it, particularly when A and B are in different computers, is to have A create a new object, M, and send it over to B. REST simply formalizes that process.

0

If I understand you correctly, I think that what you have is a more of a 'business rule' determination issue than a technical issue.

The fact that an article can be overwritten could be resolved by introducing authorization levels where senior users can override junior user's versions.Also by introducing versions as well as a column to capture the state of the article (e.g. 'in development', 'final', etc.), you could overcome this. You also may give the user the ability to select a given version either by a combination of time of submission and by the version number.

In all of the above cases, your service need to implement the business rules you set. So you could call the service with the parameters: userid, article, version, action (where the version is optional, again this depends on your business rules).

NoChance
  • 12,532
0

Since the standard behaviors of PUT, GET, and DELETE map roughly to the CRUD paradigm, some believe that Resource APIs should only be used for CRUD use cases. This, however, is an incorrect assessment because POST can be used to execute behaviors that don’t map well to CRUD.