Managing RESTful URLs in Django Rest Framework
Tags: djangorestframework, drf, django, python
Published: 2017-08-14 05:37:47

---
title: Managing RESTful URLs in Django Rest Framework
published: true
description: How to map multiple views in one URL
tags: django_rest_framework, drf, django, python
---

We've all been taught about RESTful API design. It does not take much to realize that these endpoints
```text
POST /products/1/delete
POST /products/1/update
GET /products/1
```
are inferior to
```text
DELETE /products/1
PUT /products/1
GET /products/1
```
You could also imagine that these multiple URLs per object stack up quickly and if we were to introduce more objects like `/merchants/`, `/shops/` we'd quickly be managing a lot of URLs which can get confusing. Nobody wants to read a 100-line _urls.py_ file.

But Django Rest Framework does not support mapping the same URL to different class-views based on the request method. How could we map this one URL with different methods in our _urls.py_ file?


Let's first create the initial project with our bad URLs

# Initial Project
We'll have a dead simple project which allows us to interact with product objects. We want to be able to update a product, get information about it and delete it.

_models.py_
```python3
from django.db import models
from rest_framework import serializers


class Product(models.Model):
    name = models.CharField(max_length=500)
    price = models.DecimalField(decimal_places=2, max_digits=5)
    stock = models.IntegerField()


class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = '__all__'
```

_views.py_
```python3
from rest_framework.generics import DestroyAPIView, UpdateAPIView, RetrieveAPIView

from restful_example.models import Product, ProductSerializer


class ProductDestroyView(DestroyAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer


class ProductUpdateView(UpdateAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer


class ProductDetailsView(RetrieveAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
```

and our ugly _urls.py_
```python3
from django.conf.urls import url
from django.contrib import admin
from restful_example.views import ProductDestroyView, ProductUpdateView, ProductDetailsView

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^products/(?P<pk>\d+)/delete$', ProductDestroyView.as_view()),
    url(r'^products/(?P<pk>\d+)/update$', ProductUpdateView.as_view()),
    url(r'^products/(?P<pk>\d+)$', ProductDetailsView.as_view()),
]
```

Wait, don't forget tests!

_tests.py_
```python3
from rest_framework.test import APITestCase

from restful_example.models import Product, ProductSerializer


class ProductTests(APITestCase):
    def test_can_get_product_details(self):
        product = Product.objects.create(name='Apple Watch', price=500, stock=3)
        response = self.client.get(f'/products/{product.id}')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data, ProductSerializer(instance=product).data)

    def test_can_delete_product(self):
        product = Product.objects.create(name='Apple Watch', price=500, stock=3)
        response = self.client.delete(f'/products/{product.id}/delete')
        self.assertEqual(response.status_code, 204)
        self.assertEqual(Product.objects.count(), 0)

    def test_can_update_product(self):
        product = Product.objects.create(name='Apple Watch', price=500, stock=3)
        response = self.client.patch(f'/products/{product.id}/update', data={'name': 'Samsung Watch'})
        product.refresh_from_db()
        self.assertEqual(response.status_code, 200)
        self.assertEqual(product.name, 'Samsung Watch')

```

Run our tests and you'd see that this works.


# Becoming more RESTful

Okay, now it's time to fix our URLs into more sensible ones. As we said, we want all of these views to point to one exact URL and differ only by the method they allow.

The idea here is to have some sort of base view to which the request on the url gets sent to. This view will have the job to figure out which view should handle the request given its method and send it there.
```python3
class BaseManageView(APIView):
    """
    The base class for ManageViews
        A ManageView is a view which is used to dispatch the requests to the appropriate views
        This is done so that we can use one URL with different methods (GET, PUT, etc)
    """
    def dispatch(self, request, *args, **kwargs):
        if not hasattr(self, 'VIEWS_BY_METHOD'):
            raise Exception('VIEWS_BY_METHOD static dictionary variable must be defined on a ManageView class!')
        if request.method in self.VIEWS_BY_METHOD:
            return self.VIEWS_BY_METHOD[request.method]()(request, *args, **kwargs)

        return Response(status=405)
```
This simple class requires us to inherit it and define a class variable named `VIEWS_BY_METHOD`. A dictionary which will hold our method names and their appropriate handlers.
Using this base class, creating the ManageView class for our Product model is trivial:
```python3
class ProductManageView(BaseManageView):
    VIEWS_BY_METHOD = {
        'DELETE': ProductDestroyView.as_view,
        'GET': ProductDetailsView.as_view,
        'PUT': ProductUpdateView.as_view,
        'PATCH': ProductUpdateView.as_view
    }
```
It is worth mentioning here that any [permission classes](http://www.django-rest-framework.org/api-guide/permissions/) must be defined in the separate views and will not work if put in the `ManageView`

We need to quickly edit our __urls.py__'s `urlpatterns`
```python3
urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^products/(?P<pk>\d+)$', ProductManageView.as_view()),  # this now points to the manage view
]
```

Let's test this new view as well
__tests.py__
```python3
class ProductManageViewTests(APITestCase):
    def test_method_pairing(self):
        self.assertEqual(len(ProductManageView.VIEWS_BY_METHOD.keys()), 4)  # we only support 4 methods
        self.assertEqual(ProductManageView.VIEWS_BY_METHOD['DELETE'], ProductDestroyView.as_view)
        self.assertEqual(ProductManageView.VIEWS_BY_METHOD['GET'], ProductDetailsView.as_view)
        self.assertEqual(ProductManageView.VIEWS_BY_METHOD['PUT'], ProductUpdateView.as_view)
        self.assertEqual(ProductManageView.VIEWS_BY_METHOD['PATCH'], ProductUpdateView.as_view)

    def test_non_supported_method_returns_405(self):
        product = Product.objects.create(name='Apple Watch', price=500, stock=3)
        response = self.client.post(f'/products/{product.id}')
        self.assertEqual(response.status_code, 405)
```
We change the previous tests' urls to use the new one and we can see that they pass
```
Creating test database for alias 'default'...
...
----------------------------------------------------------------------
Ran 3 tests in 0.037s

OK
Destroying test database for alias 'default'...
```
And voila, we have the same functionality but in one url! 

# Summary
What we did was create a main view which dispatches requests to the appropriate views given the request method. I believe that this is the right way to handle multiple methods per URL in DRF when you want to have different _class-based_ views handling each method.

This, however, is not the optimal case with our dead simple example. For views which are somewhat to extremely generic (like ours, no custom logic inside) or function views, there are Routers and ViewSets


# Bonus - Routers and ViewSets

Since DRF is awesome, they provide us with the ability to define all the basic operations on a model in just a few lines.
With a ViewSet class, we can define our create, read, update and destroy logic without writing any code.

_views.py_
```python3
from rest_framework import viewsets
class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
```
and we need to define the URL in our urls.py using a Router class
```python3
from rest_framework.routers import DefaultRouter
router = DefaultRouter(trailing_slash=False)
router.register(r'products', restful_views.ProductViewSet)

urlpatterns = [
    url(r'^admin/', admin.site.urls),
]

urlpatterns += router.urls

```

This viewset, believe it or not, has all the functionality we implemented above plus some additional like creating a Product(POST /products) or getting a list of all Products(GET /products)

ViewSets also allow you to override the views and create custom ones. This allows us to create function views for one URL but not class-based views.
They are absolutely amazing for our example and other simple projects, but sub-par for managing multiple class-based non-generic views with custom logic inside them.