DEV Community

Uhtred M.
Uhtred M.

Posted on • Updated on

RESTful API, Updating Object by Action Query Parameter

This article has some disadvantages and some conceptual errors that I could only notice after writing it. However, there are also beautiful advantages. I invite you to read the comments exchanged with @rouilj, and be free to participate in the discussion.

Okay, let’s dive into it! In this post I will show you how you can leave your RESTful API design more beautiful and expressive in code and documentation.

So, what I mean with: updating object by action query parameter?

Let’s suppose you have a Users resources and you need to update a user and perform some tasks (in background) after updating. Something like send new verification email, notification about username changed or something else.

Probably you will have an endpoint like: /users/{user_id}/ pointed to some view/controller (class method or function) that accepts PUT operation. Proceed with some verification to see what kind of changes are requested to do, and then you will execute the right task.

So, doing this will probably result in a lot of verification and big views and not so beautiful to document our API endpoint.

Or maybe you will add a lot of endpoints such as /users/{user_id}/change-email/, /users/{user_id}/change-username/, … and point each endpoint to respective function.

This results, but isn’t elegant as we want o to be, right? But not only because of this, having multiple functions decentralized related to same resource and objective (update) isn’t the best practice.

That’s why I suggest you to use action query parameter.

With action query parameter we can route the request to the right update (or even retrieve) function. This will bring to us a clean business logic and better API documentation.

As an example, I will show you some piece of code in Python, Django and REST Framework. But I want you to focus on business logic, so that you can implement with different language or framework.

First we will define the update action that we want to enable in a list and make some imports. Those values will be the values that we’ll expect in query parameter to route and perform the right action

# imports...
# -------------------------------------

UPDATE_ACTIONS = (
    'change-email',
    'change-username'
)

class UserDetailAPIView(APIView):
    pass
Enter fullscreen mode Exit fullscreen mode

The idea of have a UPDATE_ACTIONS list is great because it’s can bring us the flexibility to disable (if necessary) an action just by comment the action line.

Next, let’s define the default method that will handler all incoming update requests

class UserDetailAPIView(APIView):

    def put(self, request, id):
        # this will get the object and check related permissions between
        # the authenticated account and the object
        user = self.get_object(id)
        # I defined my action query parameter name as 'action'
        # but you can call something else
        if (action := request.GET.get('action')) in UPDATE_ACTIONS:
            # following python naming convention
            action_method = action.replace('-', '_')
            if hasattr(self, action_method)
                return getattr(self, action_method)(user)
            raise ValidationError(_('Update action temporarily unavailable'))
        raise ValidationError(_('Unrecognized update action'))
Enter fullscreen mode Exit fullscreen mode

As you can see, all our main update method do is get the object and check related permissions and route dynamically the action to the right method.

We also make some basic checks in case an action is unrecognized or the method isn’t available in our class.

What we have next to do is implement the action methods defined in our UPDATE_ACTION. I will show you in abstract way.

class UserDetailAPIView(APIView):

    def change_email(self, user):
        ## change email business logic
        ## run bg task
        return Response(self.serializer_class(
            user, context={'request': self.request}).data,
            status=status.HTTP_202_ACCEPTED)

    def change_username(self, user):
        ## change username business logic
        ## run bg task
        return Response(self.serializer_class(
            user, context={'request': self.request}).data,
            status=status.HTTP_202_ACCEPTED)
Enter fullscreen mode Exit fullscreen mode

So, our full class will be something like shown below.

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from rest_framework.permissions import IsAuthenticated
from rest_framework import status

from django.utils.translation import gettext_lazy as _

from user.serializers import UserDetail
from user.permissions import AuthenticatedUserIsOwner

# --------------------------------------------

UPDATE_ACTIONS = (
    'change-email',
    'change-username'
)

class UserDetailAPIView(APIView):

    serializer_class = UserDetail
    permission_classes = (
        IsAuthenticated,
        AuthenticatedUserIsOwner
    )

    # ---------------------------------------
    # API Calls

    def put(self, request, id):
        # this will get the object and check related permissions between
        # the authenticated account and the object
        user = self.get_object(id)
        # I defined my action query parameter name as 'action'
        # but you can call something else
        if (action := request.GET.get('action')) in UPDATE_ACTIONS:
            # following python naming convention
            action_method = action.replace('-', '_')
            if hasattr(self, action_method)
                return getattr(self, action_method)(user)
            raise ValidationError(_('Update action temporarily unavailable'))
        raise ValidationError(_('Unrecognized update action'))

    # ---------------------------------------
    # Update Actions

    def change_email(self, user):
        ## change email business logic
        ## run bg task
        return Response(self.serializer_class(
            user, context={'request': self.request}).data,
            status=status.HTTP_202_ACCEPTED)

    # ---------------------------------------

    def change_username(self, user):
        ## change username business logic
        ## run bg task
        return Response(self.serializer_class(
            user, context={'request': self.request}).data,
            status=status.HTTP_202_ACCEPTED)
Enter fullscreen mode Exit fullscreen mode

That's it, I hope you like this idea and help you write more elegant code with nice business logic. If you have some different idea share with me and other peoples in comments


Thanks for read, I hope you like and help you with something! I will be happy to know what you think about this. So, be sure to comment on your debt, criticism or suggestion. Enjoy and follow my work

Thanks,
Stay safe!


Top comments (7)

Collapse
 
rouilj profile image
John P. Rouillard

I have to disagree with you. Rest endpoints aren't supposed to be verbs. They are nouns. The HTTP method is the verb.

'/users/{user_id}/change-email/' would never be a rest endpoint. You would perform a PUT (or POST) on '/users/{user_id}/email' to change the value. GET retrieves the value and DELETE empties the value. This is the CRUD lifecycle.

Actions in the URL are rpc rather than RESTful.

stackoverflow.com/questions/710893... plus the original dissertation: ics.uci.edu/~fielding/pubs/dissert...

Collapse
 
uhttred profile image
Uhtred M.

Hi John, thanks to read and leave your opinion!

But, I think you did not understand me well, and that’s why I suggested using action query parameter to handle different kind of action in same resource.

Note that I’m not suggesting to use endpoint such as /users/{user_id}/change-email/ or even /users/{user_id}/email/ because I know this goes against REST specifications and in some cases email(s) could be another resource.

What I want to suggest is: leave the URI just to point to resources and use query parameter to handler different kind of actions in same resource.

Something like:

/users/{user_id}/?action=change-email
/users/{user_id}/?action=change-username

I think this is more expressive and also follow the REST specifications. And allows us to do things more dynamically

I hope this make sense for you,
Thanks 🙏🏾

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.