Creating API Actions in Django Rest Framework
Tags: python, django, opensource
Published: 2023-11-29 13:49:56

---
title: Creating API Actions in Django Rest Framework
published: true
description: Simplify API development with Django! Learn to customize action decorators for clear, efficient, and powerful web services. 🚀
tags: Python,Django,opensource
---

A few weeks back, my R&D manager asked me to create a Python package that would eventually evolve into a standalone web service. We use Django, a widely used web framework in Python, particularly known for its rest framework.

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k4wod48prlgppl871k9u.jpg)

Photo by <a href="https://unsplash.com/@faisaldada?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Faisal</a> on <a href="https://unsplash.com/photos/black-and-yellow-box-on-white-table-BI465ksrlWs?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>


Django has an impressive track record, with over 70,000 stars on GitHub and being utilized by big names like Instagram, National Geographic, Mozilla, Spotify, and Pinterest.

## Approaches

To achieve our goal, I had a couple of approaches in mind:

1. **Creating a Python Package:** This involves building a Python package with Django apps and later developing a separate web service utilizing this package.

2. **Custom Action Decorator:** Another option was to craft the action decorator, allowing API functions to be explicitly called from the ViewSet. I opted for the second choice to avoid managing two separate projects, and surprisingly, it brought additional benefits.

## Action Decorator

### Understanding the Action Decorator

Let’s start with a quick overview of the action decorator.

According to the official documentation:
> "If you have ad-hoc methods that should be routable, you can mark them as such with the @action decorator."

For example:

```python
class DummyAPIViewSet(ModelViewSet):
    # ... (previous code)

    @action(detail=True, methods=["get"], serializer_class=DummySerializer)
    def dummy(self, request, **kwargs):
        serializer = self.get_serializer(instance=self.get_object())
        return Response(data=serializer.data, status=status.HTTP_200_OK)

    @action(detail=False, methods=["post"], serializer_class=GetDummyByIntSerializer)
    def by_dummy_int(self, request, **kwargs):
        # ... (rest of the code)
```

This decorator is very straightforward. It defines a route in a ViewSet and usually requires the following arguments:

- `detail`: whether the call expects to get PK or not.
- `methods`: which methods the call supports.
- `serializer_class`: the serializer that handles the specific view.

### Custom Action Decorator

I chose to customize this decorator because it is frequently used and easy to replace with a new one.

```python
def action_api(methods=None, detail=None, url_path=None, url_name=None, **kwargs):
    def decorator(func):
        return action(
            methods=methods,
            detail=detail,
            url_path=url_path,
            url_name=url_name,
            serializer_class=serializer_class,
            **kwargs
        )(func)
```

Now, we want to call `by_dummy_int` explicitly from the ViewSet:

```python
>>> api = DummyAPIViewSet()
>>> api.dummy(request=None, pk=1)
>>> {'id': 1, 'dummy_int': 1}
```

In order to do that, we need to:

1. Create our own decorator.
2. Inject our function arguments into the method.
3. Inject our serializer into the view.

### Create Our Own Decorator

In the action implementation, we see that the following decorator gets the following arguments:

```python
action(methods=None, detail=None, url_path=None, url_name=None, **kwargs)
```

So we will create the same:

```python
def action_api(methods=None, detail=None, url_path=None, url_name=None, **kwargs):
    def decorator(func):
        return action(
            methods=methods,
            detail=detail,
            url_path=url_path,
            url_name=url_name,
            serializer_class=serializer_class,
            **kwargs
        )(func)
```

Now we want to distinguish between an API call and a REST call. We can achieve that by indicating whether the `func` argument `request` is `None` or not:

```python
def action_api(methods=None, detail=None, url_path=None, url_name=None, **kwargs):
    def decorator(func):
        def route_call(self, request, **params):
            if request:
                return func(self, request, **params)
            else:
                pass

        return action(
            methods=methods,
            detail=detail,
            url_path=url_path,
            url_name=url_name,
            serializer_class=serializer_class,
            **kwargs
        )(route_call)
```

The `route_call` mocks `func` method with the same arguments:
- `self`
- `request`
- `kwargs`

If you run this app as a server, it will behave the same.

### Inject Function Arguments into the Request

As mentioned before, a ViewSet method has 3 arguments:

- `self`
- `request`
- `kwargs`

`self` we get from the instance, so we need to mock a request object. We want to pass the function arguments into the request `data`/`query_params`.

According to the DRF code, the Request object has two relevant properties:

- `data`: POST payload
- `query_params`: GET query parameters

```python
class CustomRequest:
    """
    Mock for a custom request
    """

    def __init__(self, data, query_params):
        self.data = data
        self.query_params = query_params

def action_api(methods=None, detail=None, url_path=None, url_name=None, **kwargs):
    def decorator(func):
        def route_call(self, request, **params):
            if request:
                return func(self, request, **params)
            else:
                # injecting our custom request
                request = CustomRequest(params, params)
                return func(self, request, **params)

        return action(
            methods=methods,
            detail=detail,
            url_path=url_path,
            url_name=url_name,
            serializer_class=serializer_class,
            **kwargs
        )(route_call)
```

Let’s try it out!

```python
>>> res = api.dummy(request=None, pk=1)
ret = func(self, request, **kw)
# (output)
```

It means that the ViewSet also has a request attribute! Let’s add it:

```python
class CustomRequest:
    """
    Mock for a custom request
    """

    def __init__(self, data, query_params):
        self.data = data
        self.query_params = query_params

def action_api(methods=None, detail=None, url_path=None, url_name=None, **kwargs):
    def decorator(func):
        def route_call(self, request, **params):
            if request:
                return func(self, request, **params)
            else:
                # injecting our custom request
                request = CustomRequest(params, params)
                self.request = request
                return func(self, request, **params)

        return action(
            methods=methods,
            detail=detail,
            url_path=url_path,
            url_name=url_name,
            serializer_class=serializer_class,
            **kwargs
        )(route_call)
```

And when trying again:

```python
>>> res = api.dummy(request=None, pk=1)
>>> {'id': 1, 'dummy_int': 1}
```

We made it! but it’s enough, we want each method to know the `serializer_class` and stop using the general one.

### Inject Our Serializer into the View

We want to inject each method with its corresponding serializer. To achieve it we need to do

 two things that are connected:

1. Inject `serializer_class` to the ViewSet `kwargs`:

```python
class CustomRequest:
    """
    Mock for a custom request
    """

    def __init__(self, data, query_params):
        self.data = data
        self.query_params = query_params

def action_api(methods=None, detail=None, url_path=None, url_name=None, **kwargs):
    # popping `serializer_class` out
    serializer_class = kwargs.pop("serializer_class")
    def decorator(func):
        def route_call(self, request, **params):
            if request:
                return func(self, request, **params)
            else:
                request = CustomRequest(params, params)
                self.request = request
                # add `serializer_class` to ViewSet `kwargs`
                params.update({"serializer_class": serializer_class})
                self.kwargs = params
                return func(self, request, **params)

        return action(
            methods=methods,
            detail=detail,
            url_path=url_path,
            url_name=url_name,
            serializer_class=serializer_class,
            **kwargs
        )(route_call)
```

2. Override `get_serializer` method of `GenericViewSet`:

```python
class APIRestMixin(viewsets.GenericViewSet):
    """
    creates our custom get_serializer in order to use our serializer injection
    """

    def get_serializer(self, request_or_query_set=None, *args, **kwargs):
        serializer_class = self.kwargs.get("serializer_class", self.serializer_class)
        return serializer_class(*args, **kwargs)
```

Let’s connect everything together:

```python
class CustomRequest:
    """
    Mock for a custom request
    """

    def __init__(self, data, query_params):
        self.data = data
        self.query_params = query_params

def action_api(methods=None, detail=None, url_path=None, url_name=None, **kwargs):
    def decorator(func):
        def route_call(self, request, **params):
            if request:
                return func(self, request, **params)
            else:
                # injecting our custom request
                request = CustomRequest(params, params)
                self.request = request
                # add `serializer_class` to ViewSet `kwargs`
                params.update({"serializer_class": serializer_class})
                self.kwargs = params
                return func(self, request, **params)

        return action(
            methods=methods,
            detail=detail,
            url_path=url_path,
            url_name=url_name,
            serializer_class=serializer_class,
            **kwargs
        )(route_call)


class APIRestMixin(viewsets.GenericViewSet):
    """
    creates our custom get_serializer in order to use our serializer injection
    """

    def get_serializer(self, request_or_query_set=None, *args, **kwargs):
        serializer_class = self.kwargs.get("serializer_class", self.serializer_class)
        return serializer_class(*args, **kwargs)


class DummyAPIViewSet(APIRestMixin, ModelViewSet):
    queryset = DummyModel.objects.all()
    serializer_class = DummySerializer

    def get_serializer_context(self):
        return {
            "request": self.request,
            "view": self,
            "pk": self.kwargs.get("pk"),
        }

    @action_api(detail=True, methods=["get"], serializer_class=DummySerializer)
    def dummy(self, request, **kwargs):
        serializer = self.get_serializer(instance=self.get_object())
        return Response(data=serializer.data, status=status.HTTP_200_OK)

    @action_api(detail=False, methods=["post"], serializer_class=GetDummyByIntSerializer)
    def by_dummy_int(self, request, **kwargs):
        self.get_serializer(data=request.data).is_valid(raise_exception=True)
        queryset = DummyModel.objects.filter(dummy_int=request.data["dummy_int"]).order_by("id")
        serializer = self.get_serializer(queryset, many=True)
        return Response(data=serializer.data, status=status.HTTP_200_OK)
```

## Conclusion

In this article, we explored using the Django rest framework for API functions, delving into its internals, and adapting it to our needs. Leveraging rest frameworks offers various advantages such as:

- Clear separation between function signature and business logic
- Makes Django DB models accessible in other libraries/web services
- Arguments validation provided by serializers
- Internal pagination mechanism
- And many more!

I hope you found this article insightful and feel motivated to implement these practices in your projects! The full code and drf-api-action project are available [here](https://github.com/Ori-Roza/drf-api-action).

Don’t forget to give it a star if you find it helpful! 🌟