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

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'))

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)

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)

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!

18