DEV Community

Discussion on: RESTful API, Updating Object by Action Query Parameter

Collapse
 
rouilj profile image
John P. Rouillard • Edited

I understood what you were saying.

Using: /users/{user_id}/change-email/ or /users/{user_id}/?action=change-email does not make a difference. Both break REST's uniform interface expectation. You are putting the verb
into the URL/URI.

This is particularly bad when used in a simple example like this.
I presented a RESTful method that is more than capable of performing exactly the action you used as your example.

Another RESTful method would be to perform a GET on /users/1 to retrieve the JSON representation for the user along with the ETag. Modify the 'email' value in the json. Then perform a PUT on /users/1 with an If-Match header to prevent the lost update problem.

I am not a REST purist. I can believe that there are complex actions/workflows that would be difficult to represent using REST. But the example you present doesn't come close to meeting that threshold and IMO should not be promoted for RESTful interfaces.

Take a look at: dev.to/ricardo_borges/some-practic... as well.

Thread Thread
 
uhttred profile image
Uhtred M.

Thanks for your response and patience John ๐Ÿ™๐Ÿพ.

What if instead of:
PUT: /users/1/?action=change-username

I use:
PUT: /users/1/?t=email

??
This solve the verb problem?

Thread Thread
 
rouilj profile image
John P. Rouillard • Edited

How about /users/1/email. Email is a resource and REST operates on resources. There is no need for query parameters. Would you recommend using a GET on /users/1/?t=email?

If not then you shouldn't be using PUT on it either.

If there is a cache like squid or varnish in between the user and the server, a GET on /users/1/email will be cached. Anybody asking for /users/1/email will get the cached value.

A PUT to /users/1/?t=email will not invalidate the cached value for /users/1/email. But the cached value MUST be invalidated since the PUT has changed it. A PUT to /users/1/email will invalidate the cache entry created by the GET.

Query parameters can be used to change the response. Let's assume somebody had a really dumb system that expected all emails in UPPERCASE. If you wanted to support this, you could justify a GET on /users/1/email?uppercase=true to return an email in all uppercase. Note the resource is the same
/users/1/email, just the representation is modified by the query parameter. This allows proper cache cleaning/validation. I believe this is address in Fielding's dissertation.

Thread Thread
 
uhttred profile image
Uhtred M.

Definitely not.

Some points were missing me, now it makes more sense.

The problem in particular is what I was considering as a resource. I was just considering an object's resource, a line in the table and not its attributes.

If a user's email is a resource, then:
PUT: /users/1/email makes more sense.

Thank you very much John, I will later change the article to adapt it to what we are discussing.

But please consider that the idea I was trying to convey is that we use query parameters to automate certain updates. Because sometimes there are several attributes of an object that we want to update in a particular way.

Because attributes are part of the same object, having a single Class with several methods to update each attribute seems more flexible to me. And it offers us the possibility of having a single endpoint exposed to perform several different update operations on the same object.

And the query parameter was what I found most appropriate to automate these updates. But certainly this also brings some cons that should be well analyzed.

Thread Thread
 
rouilj profile image
John P. Rouillard

Because attributes are part of the same object, having a single Class with several methods to
update each attribute seems more flexible to me. And it offers us the possibility of having a single
endpoint exposed to perform several different update operations on the same object.

Your making it too difficult. If you have to update multiple attributes of an object,
perform a GET /users/1 to receive the following JSON:

{
            "address": "admin@localhost",
            "alternate_addresses": null,
            "organisation": null,
            "phone": "603-555-4423",
            "realname": "The Admin",
            "roles": "Admin,Agent",
            "theme": "red1",
            "timezone": "America/New_York",
            "username": "admin"
}
Enter fullscreen mode Exit fullscreen mode

If you want to update multiple fields, you perform a PUT on /users/1 with the changed data and proper headers (if-match for example):

{
            "address": "admin@newhost",
            "alternate_addresses": null,
            "organisation": "I am an organization",
            "phone": "603-444-1111",
            "realname": "The NEW Admin",
            "roles": "Admin",
            "theme": "blue2",
            "timezone": "America/New_York",
            "username": "admin"
}
Enter fullscreen mode Exit fullscreen mode

This changes all the attributes atomically. Either all changes happen (status code 200) or none of the changes happen (any status code in 400 or 500 range).

Now the tricky part is what happens when you have multiple objects that have to be modified atomically?
Consider transferring money from one account to another. I update the balance for /account/4 by perfomring a GET, subtracting 20 dollars from the balance and PUTting the new data. Then I make a deposit in /account/5 by updating the balance by 20 dollars.

What happens if the second transaction fails? How do you make that transaction atomic?
One way I have seen discussed is to use a transaction endpoint. You POST:

{ "amount": 20 }

to /account/transaction?from=4&to=5. (Yes the query parameters can be passed as part of the POST data, but you like query params so....) This returns a transaction ID (lets say 5) for the transaction
and performs the balance updates on the back end. Now you can GET /account/transaction/5 and see:

{
  "from_account": 4,
  "to_account": 5,
  "amount": 20,
  "status": "pending"
}
Enter fullscreen mode Exit fullscreen mode

or status complete when the transaction has completed. This is a very simple example that fits (or can be made to fit) into the REST architecture.

As I said I am not a REST purist, but quite a lot can be accomplished within the REST architecture.

Using the architecture also means that if I can retrieve the data (with GET) I also know how to change the data using PUT (simply put what I received back with changes). I also know how to create an entry by posting something that looks like what GET returns to /users.

With a query param, every one of those parameters "?email=", "?change_email", "?t=email" has to be documented and taught to the user. They can't use the standard REST methods and they can't autodiscover or guess what will work.