Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Locked learning resources

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

ViewSet Actions

00:00 In the previous lesson, I showed you nested serialization. In this lesson, I’m going to show you more about ViewSets and how to do custom actions.

00:10 A common pattern in the REST world is to declare an API that includes multiple objects. Up until now, the ViewSets and the routers you’ve seen have been object-specific.

00:21 If you were going to be creating a single-page web application, you might want one REST call that includes all of the objects that you want back. So it would contain a dictionary and the dictionary would then have keys for each type of object that’s coming back in your page.

00:38 The ViewSets methods that I’ve shown you up until now have been object-specific, so that technique won’t work in this case. Instead, I’m going to start by showing you how to declare a view and nest those multiple serializers inside of it. New lesson, new app.

00:55 This time, I’m going to create a new app called api. As always with a new app, you need to install it in the INSTALLED_APPS listing, add the path for the app into the urls file, and just to throw you a curve ball this time, I’m going to add a little wrinkle.

01:12 It’s a good idea to include a version number in your API path. This allows you to do backwardly-incompatible changes with your API by declaring a new version number, while maintaining backward compatibility while someone hits the old URLs.

01:28 An easy way of doing this is inside of the path() declaration inside of your urls file. I’ll show you what I mean in a moment. As before, if you’re coding along with me, the changes to the views and urls files will need to be done in conjunction. For the moment, I’m going to be using the data that’s already in the database, so you shouldn’t need to do any migrations this time. Here’s the the new view that I’m going to include inside of my api app.

01:56 Let me just scroll down.

01:59 The view starts on line 12 with the @api_view decorator indicating that this is going to be a GET view. This view is going to include two different types of information, both the doctors in the Person objects and the vehicles. For a little variety, this time I’m changing the queryset that’s being serialized to be just the doctors, so I’m filtering on anyone with the title="Dr.". And one other complication arises here as well. I hinted at this in the previous lesson.

02:30 You may recall that the VehicleSerializer uses the url field. In order to properly populate the url field, the serializer needs to be aware of the request. When you use a ViewSet, the ViewSet does this automatically for you. Because I’m not using a ViewSet here, I need to pass extra information into the VehicleSerializer so that it can see the request object. This is done through the context parameter.

02:58 The context parameter is similar to the context parameter that you would use in a Django template. It’s a dictionary containing, in this case, just the "request" field.

03:08 So, the serializer includes the vehicles being serialized. Because it’s multiple vehicles, many=True, as well as the context that includes the request object. The serializer then uses the request object out of the context to construct the host name inside of the URL so that you can get a fully qualified URL inside of the serialized payload. On line 23, I’m using the PersonSerializer to serialize the doctors queryset.

03:38 Notice that what goes in the dictionary is actually the .data attribute, so I have a shortcut here. Rather than creating a object called the serializer and then calling .data on the object, I’m just doing it directly on the constructor in one line. Then on line 24, I serialize the vehicles, like you’ve seen me do before, and return all of this inside of a REST Response object.

04:04 Inside the api app, I create an urls file and I register the listing that I just created. Notice that I’ve named the path "v1/listing/".

04:14 This is me versioning the API. In the future, if I were going to create a new backwardly incompatible listing/, I could continue to use the old code under v1/ and create a new path under v2/.

04:28 This allows me to deprecate an old API without removing it yet, maintaining backward compatibility for my users.

04:38 In a window offscreen, I’m running the Django development server, and here I’m going to hit it with curl. Version 1 of the api, piping it through the json pretty printer,

04:51 and there’s the results. Let me just scroll this back up so you can see everything. The return is a dictionary containing the field "doctors" and the field "vehicles".

05:02 The "doctors" field contains an array with all of the doctors, and the "vehicles" field contains an array with all of the vehicles. Scrolling back down, as before, the "vehicles" includes the nested "part_set" inside of it.

05:17 And notice the URLs being used in both the vehicle and part objects.

05:23 Because I used a view instead of a ViewSet that time, I’m not able to register the view with a router. As a result, you don’t get the meta listing of all of the possible views in the root path.

05:37 But what if I wanted it? Well, you can do anything with a ViewSet that you could do with a view, so this is possible.

05:46 Here’s a new ViewSet that I’ve added to the views file. I’m not doing anything here that you haven’t seen before. I’m just doing it in a slightly different format.

05:55 The declaration of the class of the DoctorsViewSet inherits from two classes. The first class is the GenericViewSet. Because this is a ViewSet, that kind of makes sense.

06:07 The second is a mixin. This mixin indicates which actions, and therefore which HTTP methods, will be supported by this ViewSet. In this particular case, all I’m including is the list.

06:21 So this ViewSet will only support listing objects. For each mixin that you include, you will have a corresponding method. For the ListModelMixin, I have the .list() method. Inside this .list() method, I’m not doing anything you haven’t seen before.

06:36 I’m serializing the doctors, putting those inside of results, and returning a REST Response.

06:45 Here’s the new api/urls file. Since I’m now using a ViewSet instead of a view, I use the router to register it, and I include all the URLs provided by the router—in this case, this will only be one—under the "v1/" path listing, giving me version numbers for my API.

07:06 Once again hitting the dev server with curl and pretty printing it,

07:13 and there’s the result. I’ll miss you, Sir Connery.

07:18 The DRF provides an entire hierarchy of ViewSet classes for your use. There’s the base ViewSet class, the GenericViewSet class that I just showed you—which adds the .queryset attribute, the .get_object() and .get_queryset() methods—the ModelViewSet that you’ve been using in the lessons up until now, and there’s also a ReadOnlyModelViewSet that is a ModelViewSet but doesn’t allow changes to it. Several lessons ago when I introduced ViewSets, I showed you this base class. It’s probably worth reviewing now. The base ViewSet declares six methods that correspond to the actions that are being performed in REST. .list() lists objects. .create() is for a POST, creating an object. .retrieve() is to get a specific object. .update() and .partial_update() are for updating existing objects. And .destroy() is for deleting an object.

08:19 Each of these methods is declared inside of a mixin. It’s done this way so you can mix and match the mixins and create a ViewSet with only those methods that you wish to support. The mixins are CreateModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixinwhich provides both .update() and .partial_update() methods—and DestroyModelMixin.

08:42 Essentially, there’s a mixin for each one of those methods I just showed you in the base class. Additionally, if there’s something that you want to do that isn’t covered by one of those actions, you can declare your own.

08:55 You do this by adding a method to a ViewSet subclass and then decorating it, indicating that it’s an action. One of the more common purposes for this is mass operations.

09:06 REST, by default, doesn’t support the change of more than one object at a time. For example, if you wanted to be able to delete multiple objects, you would have to make multiple calls. Instead, you can create an action which is .mass_delete(). Let me show you how to do that right now.

09:26 Here’s my mass delete ViewSet that I’m adding to the views file. First off, I have to inherit from GenericViewSet so that this is a ViewSet.

09:36 Secondly, this ViewSet is only going to support deleting objects, so I’m going to use the DestroyModelMixin. Inside of the class, I’m creating a method called .mass_delete(), and I tell DRF that I want this to be an action available in the REST API by decorating it with the @action decorator.

09:55 I’m passing two arguments to the decorator. The first is detail=False. This tells the DRF that the URL to be used with this action does not include an ID. Because I’m going to be deleting multiple objects, I just want a regular URL—I don’t want /3 on the end of it.

10:14 Secondly, I’m passing in a list of the HTTP methods that I’m supporting in this action. In this case, it’s only "delete". Because I’m deleting more than one thing, I somehow have to get multiple IDs up to the server.

10:30 There’s different ways of handling this. I could use GET with query parameters, or, in this case, what I’m showing you is using a POST with a string inside of it that is comma-separated. When this action is called, I will process the POST field named "ids" and split it on any commas (,) inside of it.

10:50 Each value that is found is assumed to be the id of an Artifact that needs to be deleted. I fetch that Artifact and then call the .delete() method on it. Finally, because there’s no payload that I want to return, I just send back an empty Response object.

11:09 And here’s the updated urls file. You’ll need to register the MassDeleteArtifactsViewSet with your router, adding it to the already existing doctors registration.

11:23 Let me run a query just to show you what data is available for the artifacts before I delete anything.

11:31 And there are my two artifacts. And now I will call .mass_delete(). I specify -X DELETE so that I use the HTTP DELETE method with curl, and the -d parameter shows the POST fields that I’m going to be sending in of "1,2". Notice that the URL is a little weird, that you’ve got the mass_delete/ twice.

11:54 This has to do with how the router constructs URL names. It’s based on both the class name and the method that is attached to it. There are things you can do to modify this, but you kind of have to jump through hoops. It’s easier just to accept it as it is. Finally, to show you that this worked, I’ll run the GET again.

12:17 And object 1 and 2 have been deleted, so there’s nothing left.

12:23 Next up, I’m going to leave Fedora behind and show you a new sample application that’s almost a single-page application to show you how this works in the real world.

Avatar image for renaudcepre

renaudcepre on Feb. 12, 2022

It is really a best practice to have only one endpoint with multiple types of objects returned ? It assume that the back-end have an idea of what the client want to do with the data, which is not my idea of how a REST architecture should work. From my perspective, the client should make one request to get the person list, and another for the vehicles, which allow to not write another specific endpoint, and keep DRY.


Avatar image for Christopher Trudeau

Christopher Trudeau RP Team on Feb. 13, 2022

Hi @renaudcepre,

As with all things that are design questions, the answer is “it depends”.

If you’re writing an API for others to consume you probably want to have an end point for each object and the plurals of the objects, giving the consumer of your API the maximum choice.

The problem with this approach is that it may mean a lot of network calls. This is actually why competitors to REST like GraphQL make a point of allowing the consumer to tell the API what fields they’re interested in.

My most common use case for using REST is for writing Single Page Applications. In this case, I know exactly what data I want when. Bundling things together like this means a single network call. If I’m writing an SPA it really is just me and the other developers calling the API, so it gets customized to include exactly and only what we need for the interface.

Avatar image for renaudcepre

renaudcepre on Feb. 13, 2022

Okay, I understand the idea. Thanks for your quick response, and for your tutorial, which is really helpful to have a good overview of DRF possibilities.

Avatar image for TomDevUK

TomDevUK on March 24, 2023

I may be asking something stupidly obvious but I’m learning Django at the same time and I may be missing some fundamental concepts.

If I have a model that takes some of the fields from POST and the rest of the fields are filled by calling external API where is the best place to implement logic for it? My approach was to use ViewSet and override the methods to manipulate the data before passing the data to serializer. Is this correct or should this king of logic be implemented somewhere in the serializer class?

Avatar image for Christopher Trudeau

Christopher Trudeau RP Team on March 25, 2023

Hi Tom,

I’m not sure I follow your question. I think you’re saying you have an API view that gets called by POST, then inside that you are doing a further external call to get more information.

If that’s the case, then yes, the place I’d put that logic is where ever I’m handling the API view, so if that is in a ViewSet, yep that’d be where it should go.

Something to consider though, your API call won’t return until you’re done here, which means the person doing the POST is waiting on you doing your external call. Depending on how responsive the external system is, that may not be the best choice. Unfortunately, the other choices are more complicated from a coding stand-point: either threads or queueing the work to be done by another process. Both of these situations mean that the response to the POST has to be “yeah, I’ll get around to it, here is an ID to check later if it is done”.

Hope this helps. Post back if I didn’t understand the question.

Avatar image for TomDevUK

TomDevUK on March 27, 2023

Thanks Christopher! That is very helpful. This is exactly what I’ve done but started second guessing myself thinking this should be implemented somewhere in the serializer itself. The wait is unavoidable as the external API call in effect verifies if the data submitted in POST is valid.

Avatar image for Christopher Trudeau

Christopher Trudeau RP Team on March 27, 2023

You’re welcome Tom. I’m not sure it makes much difference, but my mental model is aligned to how you would use a view without an API. A user submitting a form would result in something new added to your database – and that’d all be done in the view.

The serializer on the other hand is a translator, so I tend to keep it responsible for going from the one format to the other.

At risk of sending you down a rabbit-hole, you should check out the Django Ninja framework. It is my preference these days for this kind of API. It is a lot like FastAPI and tends to require a lot less code. It isn’t quite as comprehensive as DRF, but if your requirements don’t need the fanciest stuff, it gets you there way faster.

Avatar image for TomDevUK

TomDevUK on March 29, 2023

Interesting re Ninja, I’ll go through your Sneaky Rest API’s course and see how it works :)

Become a Member to join the conversation.