The Django Rest Framework (DRF), has a lot of built in functionality that supports CRUD operations, but building an RPC endpoint requires hand-rolling much of that. Ultimately, if we’re adding an RPC endpoint to an existing API with mostly REST endpoints, we want to match the design of our new endpoint to match that of the DRF, so we need to understand what each piece does.
All of these pieces are explained in the DRF docs, but seeing a complete example and how they all work together can add clarity.
I’m going to use the example here of fulfilling a purchase. In this example, I do have a Purchase
database object that will be modified in the process. However, because other things happen too — the user is emailed a receipt, other database objects are created or modified (a course registration is created, for example), etc., this deviates from a typical REST endpoint.
So before we jump in, here’s the spoiler of what’s going to happen, and links to the relevant docs:
super
and hit the mixinLet’s look at each of these in more detail.
First, we need to register the urls that our users can call. My urls.py file looks like this:
from rest_framework.routers import SimpleRouter
from api.v1.purchases.views import Purchase
router = SimpleRouter(trailing_slash=False)
router.register(r’’, Purchase, base_name=’purchase’)
urlpatterns = router.urls
In this case, the Purchase
that I’m registering as the view to call, is referring to a class based view, which is defined and described in more detailed below, and is different from the Purchase
model.
This means that there will actually be an endpoint available for each of the methods defined in that class. This urls file is included in my general api/v1
urls file.
The class described below contains both my REST endpoints, which use the ListModelMixin
, RetrieveModelMixin
, etc., that are defined by DRF. I’ve included my custom PurchaseFulfillmentMixin
, that will do similar work for that endpoint.
from rest_framework import mixins
from rest_framework.decorators import detail_route
class Purchase(mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
PurchaseFulfillmentMixin):
lookup_field = 'purchase_id'
lookup_value_regex = '[0-9a-z]+'
serializer_class = PurchaseSerializer
def get_serializer_class(self):
if self.action == 'fulfill':
return PurchaseFulfillmentSerializer
return self.serializer_class
def get_queryset(self):
# You have access to self.request here
...
# This class also included methods for each of the basic CRUD operations, which take advantage of ListModelMixin, etc.
@detail_route(methods=['post']
def fulfill(self, request, *args, **kwargs):
return super(Purchase, self).fulfill(request, *args, **kwargs)
Defining get_serializer_class
allows me to have a default serializer that is used for all of my CRUD operations, while defining my own custom one for my RPC endpoint, which is returned if the fulfill endpoint is called.
Using a detail_route allows me to include an RPC endpoint within the existing framework of my CRUD ones. We’re matching the structure! Calling it a detail_route
means that the URL generated by this endpoint expects the primary key of the instance to be included (i.e. purchases//fulfill
), which means I can call get_object
, and have it work (more on that in the next section).
The fulfill
view just calls super, and will find the fulfill
method on the customPurchaseFulfillmentMixin
, explained next, and we’re on our way!
My custom mixin mimics the behavior in the other mixins included in my view class. Looking at the DRF source code for the built-in mixins, you’ll see they are primarily responsible for:
So without further ado, my custom mixin:
from django.core.exceptions import PermissionDenied
from rest_framework.response import Response
class PurchaseFulfillmentMixin(object):
def fulfill(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.fulfill(instance, *args, **kwargs)
return Response(serializer.data)
detail_route
and my url includes a primary key, I can call get_object
, which first calls my self-defined get_queryset
method, and then looks within that for the object with the primary key from the path.validated_data
, as noted below.ChoiceField
. This means that if someone calls my endpoint with a value in the state field that wasn’t included in the options, the serializer will fail validation and because I’ve called it with raise_exception=True
, a 400 response will be returned.Serializers allow you to define the fields that are available to your users in both the request and the response, and holds most of the logic. By default, fields have both read and write access. In my case, I wanted very different fields in my request and response, so all of my fields have either read_only=True
(will be included in the response, but not accessible to the user via the request), or write_only=True
(will be accessible via the POST data in the request, but will not be included in the response).
from rest_framework import serializers, fields
FULFILLMENT_STATE_CHOICES = ...
class PurchaseSerializer(serializers.ModelSerializer):
...
class Meta:
model = Purchase # the database object, not the class-based view
...
class PurchaseFulfillmentSerializer(serializers.Serializer):
purchase_state = fields.ChoiceField(choices=FULFILLMENT_STATE_CHOICES, write_only=True)
redirect_url = fields.SerializerMethodField()
error_msg = fields.CharField(required=False, write_only=True)
purchase = PurchaseSerializer(source='*', read_only=True)
def get_redirect_url(self, purchase):
...
def fulfill(self, instance, *args, **kwargs):
# Do the thing
return instance
In addition to being used for the CRUD endpoints, as discussed in the view section, the PurchaseSerializer
is used for a field in the PurchaseFulfillmentSerializer
, and is a standard ModelSerializer
, so not discussed in detail in this post.
At this point, the main thing left is to do the actual logic of fulfilling. I’ve not included this, since it’s not relevant to the design of this endpoint, so a few notes:
self.validated_data
within your serializer, which means you can, of course, make decisions within your functions based on what the user has passed in.SerializerMethodFields
are somewhat beyond the scope of this post, but basically allow you to define a field which will then look for a method by the name get_<field_name>
and will use the return value of that function as the value for the field. Docs are here.instance.save()
Note: I include imports for things external to my project in code snippets to help others who might use them as an example know where to look, but don’t include imports from within my project as they aren’t relevant — it is implied that everything is properly imported.