AI-translated. Read the original in Korean
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_classspecifies 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_classconfigured:{ count: 1000, results: […] }
-
Response structure without
pagination_class:[…]
-
-
filter_backendsdefines 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
searchparameter, for example: www.superbong.com?search=daniel search_fieldsspecifies the fields to search against that value.
- Filters using the value of the URL's
- OrderingFilter
- Orders by the field named in the URL's
orderingparameter. A leading-means descending order. For example: www.superbong.com?ordering=-id ordering_fieldsdefines the fields available for ordering.
- Orders by the field named in the URL's
- DjangoFilterBackend
- Use this when you want to filter by field names in the URL.
- Add parameters in the form
column=value. filter_fieldsspecifies which fields may be used as columns.- Other column names are ignored.
- SearchFilter
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.