Recently, we used Django REST Framework to build the backend for an API-first web application. Here I’ll attempt to explain why we chose REST Framework and how successfully it helped us build our software.
Why Use Django REST Framework?
RFC-compliant HTTP Response Codes
If authentication fails, REST Framework serves a 401 response. Raise a PermissionDenied and you automatically get a 403 response. Raise a ValidationError when examining the submitted data and you get a 400 response. POST successfully and get a 201, PATCH and get a 200. And so on.
You could PATCH an existing user profile with just the field that was changed in your UI, DELETE a comment, PUT a new shopping basket, and so on. HTTP methods exist so that you don’t have to encode the nature of your request within the body of your request. REST Framework has support for these methods natively in its base ViewSet class which is used to build each of your endpoints; verbs are mapped to methods on your view class which, by default, are implemented to do everything you’d expect (create, update, delete).
The base ViewSet class looks for the Accepts header and encodes the response accordingly. You need only specify which formats you wish to support in your
Serializers are not Forms
Django Forms do not provide a sufficient abstraction to handle object PATCHing (only PUT) and cannot encode more complex, nested data structures. The latter limitation lies with HTTP, not with Django Forms; HTTP forms cannot natively encode nested data structures (both
multipart/form-data rely on flat key-value formats). Therefore, if you want to declaratively define a schema for the data submitted by your users, you’ll find life a lot easier if you discard Django Forms and use REST Framework’s
Serializer class instead.
If the consumers of your API wish to use PATCH rather than PUT, and chances are they will, you’ll need to account for that in your validation. The REST Framework
ModelSerializer class adds fields that map automatically to
Field types, in much the same way that Django’s
ModelForm does. Serializers also allow nesting of other Serializers for representing fields from related resources, providing an alternative to referencing them with a unique identifier or hyperlink.
Should you choose to go beyond an AJAX-enabled site and implement a fully-documented, public API then best practice and an RFC or two suggest that you make your API discoverable by allowing OPTIONS requests. REST Framework allows an OPTIONS request to be made on every endpoint, for which it examines
request.user and returns the HTTP methods available to that user, and the schema required for making requests with each one.
Support for OAuth 1 and 2 is available out of the box and OAuth permissions, should you choose to use them, can be configured as a permissions backend.
REST framework provides a browsable HTTP interface that presents your API as a series of forms that you can submit to. We found it incredibly useful for development but found it a bit too rough around the edges to offer as an aid for third parties wishing to explore the API. We therefore used the following snippet in our
settings.py file to make the browsable API available only when
DEBUG is set to
if DEBUG: REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'].append( 'rest_framework.renderers.BrowsableAPIRenderer' )
REST Framework gives you an
APITestCase class which comes with a modified test client. You give this client a dictionary and encoding and it will serialise the request and deserialise the response. You only ever deal in python dictionaries and your tests will never need to contain a single instance of
The documentation is of a high quality. By copying the Django project’s three-pronged approach to documentation – tutorial, topics, and API structure, Django buffs will find it familiar and easy to parse. The tutorial quickly gives readers the feeling of accomplishment, the high-level topic-driven core of the documentation allows readers to quickly get a solid understanding of how the framework should be used, and method-by-method API documentation is very detailed, frequently offering examples of how to override existing functionality.
At the time of writing the project remains under active development. The roadmap is fairly clear and the chap in charge has a solid grasp of the state of affairs. Test coverage is good. There’s promising evidence in the issue history that creators of useful but non-essential components are encouraged to publish their work as new, separate projects, which are then linked to from the REST Framework documentation.
We found that writing permissions was messy and we had to work hard to avoid breaking DRY. An example is required. Let’s define a
ViewSet representing both a resource collection and any document from that collection:
class JobViewSet(ViewSet): """ Handles both URLS: /jobs /jobs/(?P<id>d+)/$ """ serializer_class = JobSerializer permission_classes = (IsAuthenticated, JobPermission) def get_queryset(self): if self.request.user.is_superuser: return Job.objects.all() return Job.objects.filter( Q(applications__user=request.user) | Q(reviewers__user=request.user) )
Job collection is requested, the queryset from
get_queryset() will be run through the
serializer_class and returned as an
HTTPResponse with the requested encoding.
Job item is requested and it is in the queryset from
get_queryset(), it is run through the
serializer_class and served. If a Job item is requested and is not in the queryset, the view returns a 404 status code. But we want a 403.
So if we define that JobPermission class, we can fail the object permission test, resulting in a 403 status code:
class JobPermission(Permission): def get_object_permission(self, request, view, obj): if obj in Job.objects.filter( Q(applications__user=request.user) | Q(reviewers__user=request.user)): return True return False
Not only have we duplicated the logic from the view method
get_queryset (we could admittedly reuse
view.get_queryset() but the method and underlying query would still be executed twice), if we don’t then the client is sent a completely misleading response code.
The neatest way to solve this issue seems to be to use the
DjangoObjectPermissionsFilter together with the django-guardian package. Not only will this allow you to define object permissions independently of your views, it’ll also allow you filter querysets using the same logic. Disclaimer: I’ve not tried this solution, so it might be a terrible thing to do.
REST Framework is not built to support nested resources of the form
/baskets/15/items. It requires that you keep your API flat, of the form
We did eventually choose to implement some parts of our API using nested URLs however it was hard work and we had to alter public method signatures and the data types of public attributes within our subclasses. We required entirely highly modified Router, Serializer, and ViewSet classes. It is worth noting that REST Framework deserves praise for making each of these components so pluggable.
Very specifically, the biggest issue preventing us pushing our nested resources components upstream was REST Framework’s decision to make
lookup_field on the
HyperlinkedRelatedField a single string value (e.g. “baskets”). To support any number of parent collections, we had to create a
NestedHyperlinkedIdentityField with a new
lookup_fields list attribute, e.g.
REST Framework is great. It has flaws but continues to mature as an increasingly popular open source project. I’d whole-heartedly recommend that you use it for creating full, public APIs, and also for creating a handful of endpoints for the bits of your site that need to be AJAX-enabled. It’s as lightweight as you need it to be and most of what it does, it does extremely well.
About us: Isotoma is a bespoke software development company based in York and London specialising in web apps, mobile apps and product design. If you’d like to know more you can review our work or get in touch.