What to do when you need more verbs in REST
There is another similar question to mine, but the discussion veered away from the problem I'm encounting.
Say I have a system that deals with expense reports (ER). You can create and edit them, add attachments, and approve/reject them.
An expense report might look like this:
GET /er/1
=>
{"title": "Trip to NY", "totalcost": "400 USD",
"comments": [
"john: Please add the total cost",
"mike: done, can you approve it now?"
],
"approvals": [
{"john": "Pending"}, {"finance-group": "Pending"}]
}
That looks fine, right? Thats what an expense report document looks like.
If you want to update it, you can do this:
POST /er/1
{"title": "Trip to NY 2010"}
If you want to approve it, you can do this:
POST /er/1/approval
{"approved": true}
But, what if you want to update the report and approve it at the same time? How do we do that? If you only wanted to approve, then doing a POST
to something like /er/1/approval
makes sense.
We could put a flag in the URL, POST /er/1?approve=1
, and send the data changes as the body, but that flag doesn't seem RESTful.
We could put special field to be submitted, too, but that seems a bit hacky, too. If we did that, then why not send up data with attributes like set_title
or add_to_cost
?
We could create a new resource for updating and approving, but (1) I can't think of how to name it without verbs, and (2) it doesn't seem right to name a resource based on what actions can be done to it (what happens if we add more actions?)
We could have an X-Approve: True|False header, but headers seem like the wrong tool for the job. It'd also be difficult to get set headers without using javascript in a browser.
We could use a custom media-type, application/approve+yes
, but that seems no better than creating a new resource.
We could create a temporary "batch operations" url, /er/1/batch/A
. The client then sends multiple requests, perhaps POST /er/1/batch/A
to update, then POST /er/1/batch/A/approval
to approve, then POST /er/1/batch/A/status
to end the batch. On the backend, the server queues up all the batch requests somewhere, then processes them in the same backend-transaction when it receives the "end batch processing" request. The downside with this is, obviously, that it introduces a lot of complexity.
So, what is a good, general way to solve the problem of performing multiple actions in a single request? General because its easy to imagine additional actions that might be done in the same request:
Its also an issue of performance. HTTP calls hit the network (which could be a problem if you have high latency or a flaky connection), so the fewer of them you can make, the better.
The REST architecture says that a resource is managed by the server and identified by a URL.
In that light /er/1/approval
is not a reasonable URL or model to use, unless you have an approval object or entity that you manage and manipulate on the server side. Seems to me the entity is the expense report itself, which means, /er/1
is your URL path.
Now, as for verbs... you can send (POST) any message you like to that resource.
set data:
{ action: "modify", data: { purpose : "Club hopping" } }
approve:
{ action: "approve" }
add item:
{ action:"additem", data: { amount:72.13, category:113, note:"client dinner" }}
etc.
From Fielding's Ch5, which defined REST,
The in-parameters (of a request) consist of request control data, a resource identifier indicating the target of the request, and an optional representation.
...and...
Control data defines the purpose of a message between components, such as the action being requested or the meaning of a response. It is also used to parameterize requests and override the default behavior of some connecting elements. For example, cache behavior can be modified by control data included in the request or response message.
Therefore if you'd like to perform multiple actions on a resource, then you should embed in the "control data" multiple messages or action requests. In my example, the posted data would be something like:
{ action: "modify", data: { purpose : "Club hopping" } }
{ action: "approve" }
But you'd probably want to generalize that so that it is:
{ actions: [ {action:"modify", data: {...} }, { action:"approve"} ] }
The messages or actions your server can handle on each particular type of entity are up to you to define.
ps: sometimes REST implementations use HTTP PUT
to create a resource and POST
to modify or act on an existing resource.
and: I liked the article, How to GET a cup of coffee.
For manipulating the status of resources I often like to use "status buckets". The idea is that when you "add" an object into that bucket, it gets that status. It is like having in and out boxes on your desk. The location of the document defines its status.
So, you could do something simple like:
POST /Expenses/Approved
{ .. Expense document ... }
or for the more complex case that you hinted at in your document where multiple people have to approve the document.
POST /ExpenseApprover/John/ApprovedExpenses
{ .. Expense document ... }
If you need to submit an expense report for approval you can do
POST /ExpenseApprover/John/Pending
{ .. Expense document ... }
And don't forget hypermedia can make this process workflow enabled. Imagine someone creates an initial expense report, the server could response with the following JSON.
{ "id" : "234",
"title": "Trip to NY", "totalcost": "400 USD",
"submit_url": "/ExpenseApprover/John/Pending"
}
The client can POST to the submit_url to move the expense onto it's next step. Then when John retrieves the expense, he gets
{ "id" : "234",
"title": "Trip to NY", "totalcost": "400 USD",
"approve_url": "/ExpenseApprover/Finance/Pending",
"denied_url": "/ExpenseApprover/John/Denied",
}
When the finance department do a
GET /ExpenseApprover/Finance/Pending
they could get a list of Pending Expenses,
{ PendingExpense: [
{ "id" : "234",
"title": "Trip to NY", "totalcost": "400 USD",
"approve_url": "/Expense/Approved",
"denied_url": "/ExpenseApprover/Finance/Denied",
}
]
}
Forgive my horrible JSON, but I hope you get the idea that including the link in the response you can guide the flow of your application. You can also stop worrying so much about what the url looks like because the client doesn't really care. The client reads the url from the response based on the property name and dereferences it. You can change your mind a million times on what the best url structure is and your clients will not be affected. Just don't change the property name!
These "status bucket" urls are used to hold a set of resources that have a similar status. The idea is that you POST a document into the collection:
POST /ExpenseApprover/Finance/Denied
{"id" : "234", "title": "Trip to NY", "totalcost": "400 USD"}
It is not necessary to uniquely define the particular expense that you are adding in the URL because the body document should contain some kind of identifying key value.
This technique is just as valid for flagging expenses has having discrepancies. You simply create a new resource that holds expenses with discrepancies and post your expense report into to it.
POST /Discrepancies
{"id" : "234", "title": "Trip to NY", "totalcost": "400 USD"}
What to do when you need more verbs in REST
You have 3 options:
In your case you just missed the RFC of the PATCH method by a month or two.
链接地址: http://www.djcxy.com/p/45348.html上一篇: 用于创建和编辑表单的REST URL
下一篇: 在REST中需要更多动词时该怎么做