Skip to content
:tech
← Articles

AI-translated. Read the original in Korean

How We Use Django REST Framework

How We Use Django REST Framework

1. Why we adopted the framework, and how we applied it

I recall that Buzzvil started using this framework around spring 2015, about a year and a half ago. Before that, development mainly used Django's function-based views. When a new project began, we discussed its specifications and technology choices with other engineers. We needed to separate the frontend from the backend. In exploring that, we discovered Django REST Framework for the backend and AngularJS for the frontend, and chose Django REST Framework as part of the backend stack.

First, let me explain how it addressed weaknesses in the existing code. The code below shows how the previous project returned a view. We used function-based views.

def test_view(self, request):
    return render_to_response('test', {})

This approach coupled frontend and backend code. If we needed to return something other than HTML, we had to modify all the relevant code.

Below is the code we used when we wanted to return JSON. There may have been better ways, but we kept it simple and handled a parameter.

def test_view(self,request):
    # Return JSON type
    if request.GET.get('type') == 'json':
        return HttpResponse(data, content_type='application/json')
    return render_to_response('test', {})

The same issue arose when branching on the HTTP method.

def test_view(self,request):
    # Return JSON type
    if request.method == 'POST':
        blahblah...
    if request.method == 'GET':
        return render_to_response('test', {})

At first, you might think this much manual work is manageable. But as the frontend grew more complex, requests arrived: “Please filter by ID! There is too much returned data, so we want to paginate it in the UI!” How could we accommodate all these requirements flexibly?

After some thought, I decided: “Django supports pagination, so let's use that. What about filtering? I'll just read the parameters and implement it myself. How much more could there be?” And I got to work.

The following code added filtering and Django's pagination. Notice the try…except blocks around pagination—not particularly pleasant to read, are they?

from django.core.paginator import Paginator
def test_view(self,request):
    model_list = Model.objects.all()
    # Filter
    some_filter = request.GET.get('some_filter')
    if some_filter:
        model_list = model_list.filter(some=some_filter)
    ...
    # Pagination
    paginator = Paginator(model_list, 25)
        page = request.GET.get('page')
        try:
            models = paginator.page(page)
        except PageNotAnInteger:
            models = paginator.page(1)
    return render_to_response('test', {'models': model})

All right, we had held on this far! But what about data validation? Let's use Django Forms! Then again, would we need a form every time? That thought led to the following code.

# test/forms.py
from django.forms import ModelForm
from test.models import Model

class TestForm(ModelForm):
    class Meta:
        model = Model
    def clean():
        cleaned_data = super(TestForm, self).clean()
        # add validation code
        return cleaned_data

# test.views.py
from django.core.paginator import Paginator
from test.forms import TestForm

def test_view(self,request):
    # Return JSON type
    if request.method == 'POST':
        form = TestForm(request.POST)
        if form.isvalid():
            form.save()
        else:
            blahblah
    if request.method == 'GET':

    return render_to_response('test', {'models': model})

I thought we had somehow managed to get through it, but more requirements appeared.

  • How should we restrict data according to permissions?
  • We can set permissions for URLs, but can we also control access to the data itself?
  • Can dates be returned in the user's local time?

I did not want to repeat all this work to solve those problems. Then I discovered Django REST Framework, which seemed capable of addressing these shortcomings and remaining useful in future projects.

2. About Django REST Framework

Its strengths include:

  • API support
  • Multiple authentication methods
  • Class-based implementation
  • Data restrictions based on user permissions
  • Various return formats, such as JSON, CSV, and Excel
  • Extensibility through customization

As these advantages suggest, it offered strong API-level support, allowed us to separate the frontend and backend, and fit naturally with Django. Starting a new project also felt like a good opportunity to take on a new technology, so we decided to adopt it.

Our first approach was to inherit from the class-based views in the generics package, as described in the documentation. Here is the first version. Much cleaner than before, right?

# test/urls.py
urlpatterns = patterns(
    'test.views',
    url(r'^/api/tests/(?P<pk>\d+)', TestDetailView.as_view()),
    url(r'^/api/tests', TestListView.as_view()),
)

# test/views.py
class TestListView(generics.ListCretateView):
    queryset = Model.objects.all()
    serializer_class = ModelSerializer

class TestDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Model.objects.all()
    serializer_class = ModelSerializer

This accelerated development, but as the number of URLs grew, so did the number of views and copied-and-pasted code, creating maintenance problems. Here is an example of controlling a growing set of URLs through views.

# test/urls.py

urlpatterns = patterns(
    'test.views',
    url(r'^/api/tests/(?P<pk>\d+)/pets/(?P<pk>\d+)', PetsDetailView.as_view()),
    url(r'^/api/tests/(?P<pk>\d+)/pets', PetsListView.as_view()),
    url(r'^/api/tests/(?P<pk>\d+)', TestDetailView.as_view()),
    url(r'^/api/tests', TestListView.as_view()),
)

# test/views.py
class TestListView(generics.ListCretateView):
    queryset = Model.objects.all()
    serializer_class = ModelSerializer

class TestDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Model.objects.all()
    serializer_class = ModelSerializer

class PetsListView(generics.ListCretateView):
    queryset = Pet.objects.all()
    serializer_class = PetSerializer

class PetsDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Pet.objects.all()
    serializer_class = PetSerializer

We looked for a better way to handle the maintenance and scalability issues caused by having N URLs and N views. We learned that Django REST Framework's ViewSets and Routers let us group views together, which could solve those problems.

Here is the code using a ViewSet and Router. Much tidier!

# test/urls.py

router = SimpleRouter()
router.register(r'/pets', PetViewSet)
router.register(r'/', TestViewSet)

urlpatterns = patterns(
    url(r'^/api/tests', include(router.urls))
)

# test/viewsets.py
class PetViewSet(viewsets.ModelViewSet):
    serializer_class = PetSerializer
    queryset = Pet.objects.all()

class TestViewSet(viewsets.ModelViewSet):
    serializer_class = ModelSerializer
    queryset = Model.objects.all()

3. Solving the remaining problems

Pagination and filtering were next. Django REST Framework's pagination_class, filter_fields, search_fields, and ordering_fields made those straightforward and readable too.

  • pagination_class specifies the class used for pagination. When configured, returned data is paginated. Important: the structure of the response changes depending on whether pagination is enabled.

    • Response structure with pagination_class configured:

      { count: 1000, results: […] }

    • Response structure without pagination_class:

      […]

  • filter_backends defines which filters apply to requests to the class. In the example below, I use three: SearchFilter, OrderingFilter, and DjangoFilterBackend.

    • SearchFilter
      • Filters using the value of the URL's search parameter, for example: www.superbong.com?search=daniel
      • search_fields specifies the fields to search against that value.
    • OrderingFilter
      • Orders by the field named in the URL's ordering parameter. A leading - means descending order. For example: www.superbong.com?ordering=-id
      • ordering_fields defines the fields available for ordering.
    • DjangoFilterBackend
      • Use this when you want to filter by field names in the URL.
      • Add parameters in the form column=value.
      • filter_fields specifies which fields may be used as columns.
      • Other column names are ignored.

And here it is: the final code with filtering and ordering.

# test/utils.py
class LargePagination(PageNumberPagination):
    page_size = 10
    page_size_query_param = 'page_size'
    max_page_size = 10000

# test/viewsets.py
class PetViewSet(viewsets.ModelViewSet):
    pagination_class = LargePagination
    serializer_class = PetSerializer
    queryset = Pet.objects.all()
    filter_backends = (filters.SearchFilter, filters.OrderingFilter, filters.DjangoFilterBackend, )
    filter_fields = ('filter1', )
    serach_fields = ('id', 'name', )
    ordering_fields = ('id', 'created_at',)
    ordering = ('-id')

Another strength of Django REST Framework is how easy it is to change data formats according to permissions or users. We use serializers to control formats and restrict data. As shown below, a serializer makes it easy to transform incoming client data with to_internal_value and outgoing data with to_representation.

  • to_internal_value
    • Handles incoming client data before it is saved for changes such as POST or PUT.
  • to_representation
    • Transforms values for the client when returning data through GET, POST, or PUT, including after changes.
# test/serializers.py
class AllDataReturnSerializer(serializers.ModelSerializer):
    class Meta:
        model = Model
        fields = '__all__'

    def to_representation(self, val):
        return super(AllDataReturnSerializer, self).to_representation(val)

    def to_internal_value(self, val):
        return super(AllDataReturnSerializer, self).to_internal_value(val)

class SomeDataReturnSerializer(serializers.ModelSerializer):
    class Meta:
        model = Model
        fields = (
            'some1',
            'some2',
            'some3',
        )

# test/viewsets.py
class PetViewSet(viewsets.ModelViewSet):
    …
    def get_serializer_class(self):
        if return_all_data:
            return AllDataReturnSerlaizer
        elif return_some_data:
            return SomeDataReturnSerializer

4. Closing thoughts

Django REST Framework is not perfect, but its conventions and rules have helped reduce development time and improve readability.

Since adopting it, our next major concern has been testing. As the engineering team grows, so does the code we need to maintain. Changes increasingly cause errors in unexpected places, consuming more resources.

So far, we have only been testing at the backend API level. As the frontend codebase grows, the need for tests grows too. How should we address that? Should we also write tests for the UI? These are significant questions, and we still have a lot to think through.

All articles