← Back to list

Bulk destroy in Django REST Framework (DRF) using ModelViewSet

One of the great features in DRF (Django REST Framework) is Viewset. It allows a developer to define a set of built-in simple-straight…

Sudip · 2025-01-05 22:58 · 1 claps · 5.0 min read paywalled
#django #django-rest-framework #bulk-delete #viewset #rest-api
Open on Medium ↗
Wiki topics: CRY · Crypto & Web3 🌐 · Web Development

Bulk destroy in Django REST Framework (DRF) using ModelViewSet

One of the great features in DRF (Django REST Framework) is Viewset. It allows a developer to define a set of built-in simple-straight action handlers for a resource. Once, it’s registered with the DRF url default-router, it can provide a standard set of CRUD style actions for the resource.

This article explains a way to add a support for the custom action called bulk-destroy, to perform deletion in bulk with the help of a demo application.

Experience in django & DRF would be great to understand the article better.

The challenge

In one of the projects, we used viewsets (ModelViewSet) heavily in our django application to perform simple CRUD operations on resources. There were a set of resources, where authenticated and authorized users were needed to perform a deletion operation for multiple entries/objects. If it was for a single resource, surely, we could have gone with a custom view implementation using APIView class or maybe other way. The application was growing gradually, and we needed a generic approach to tackle this challenge. Finally, it was making sense for us to design bulk-destroy action using the listing route so that it can be invoked with http DELETE method, in similar way how list can be invoked with GET method. Obviously, bulk-destroy needs to be requested with the ids of the to-be-deleted objects so that the application can verify the authority of the requesting user and can delete the requested objects successfully.

Photo by Yoko Correia Nishimiya on Unsplash

Photo by Yoko Correia Nishimiya on Unsplash

The Demo application

We’re going to take a look at a demo application which uses the bulk-destroy action. The complete codebase is available here.

I’ve designed a simple Note resource in this demo for which the django model looks like as follows:

from django.conf import settings
from django.db import models

class Note(models.Model):
    title       = models.CharField(max_length=1024, blank=True, null=True)
    body        = models.TextField(blank=True, null=True)
    created_at  = models.DateTimeField(auto_now_add=True, editable=False)
    modified_at = models.DateTimeField(auto_now=True, editable=False)
    owner       = models.ForeignKey(settings.AUTH_USER_MODEL,
                        null=True, editable=False, db_index=True,
                        on_delete=models.SET_NULL, related_name="note_owner")

    class Meta:
        db_table = "note"

    def __str__(self):
        return f"(pk: {self.pk}, title: {self.title})"

The idea is that a user can own multiple notes with given title, body. The user model is the default django auth-user one.

Now to support bulk-destroy action in form of http rest api with DELETE method, we need to update the router mapping in the DRF which exposes the urls to the client.

# router.py
from rest_framework.routers import DefaultRouter

from restapis import views

class CustomAPIRouter(DefaultRouter):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # by DRF definition, this might be subject to change to version of DRF
        list_route = self.routes[0]
        list_route.mapping.update({
            "delete": "bulk_destroy"
        })

# Routers provide an easy way of automatically determining the URL conf.
APIRouter = CustomAPIRouter()

# note API routers
APIRouter.register(r'notes', views.NoteViewSet, basename="notes")

As you can see in the above snippet, we overridden the router by inheriting the DefaultRouter from DRF. We took out the listing route and updated the mapping with delete HTTP method to the bulk_destroy view method. In other words, now a viewset can implement bulk_destroy view method to handle the multiple objects delete request for the given resource.

The class DefaultRouter inherits the SimpleRouter class. Have a look at the SimpleRouter code to understand why we took the first item from routes to consider it as a list route. More details on custom routes here.

To support the common behavior of bulk destroy action in a model based viewset, we can introduce a mixin called BulkDestroyModelMixin. In fact, ModelViewSet is nothing but a group of mixins to support basic CRUD operations for a resource. The implementation can be seen here.

# serializers

from rest_framework import serializers

class BulkDestroyInstanceSerializer(serializers.Serializer):
    """
    ids of all to be destroyed objects
    """
    ids = serializers.ListField(
        child=serializers.IntegerField(min_value=1),
        write_only=True
    )
# mixins.py

import logging

from rest_framework import status
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError, PermissionDenied

from ..serializers import BulkDestroyInstanceSerializer

log = logging.getLogger(__name__)

class BulkDestroyModelMixin:
    """
    BulkDestroyModelMixin provides the flexibility to delete
    multiple resources on the list endpoint with the delete
    method.
    """

    # in case, viewset has not defined bulk-destroy count
    __DEFAULT_BULK_DESTROY_COUNT = 5

    # safe side max count
    __MAX_BULK_DESTROY_COUNT = 10

    def bulk_destroy(self, request, *args, **kwargs):
        # if not allowed then raise the error
        if not getattr(self, "allow_bulk_destroy_method", False):
            return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)

        serializer = BulkDestroyInstanceSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        # get ids from serializer validated data
        destroy_instance_id_list = serializer.validated_data.get("ids", [])

        # validation check - at least one id is provided
        if len(destroy_instance_id_list) == 0:
            raise ValidationError("at least one object id is required to perform batch delete operation")

        # max allowed bulk destroy count
        bulk_destroy_count = getattr(self, "bulk_destroy_count", 0) or self.__DEFAULT_BULK_DESTROY_COUNT
        bulk_destroy_count = min(bulk_destroy_count, self.__MAX_BULK_DESTROY_COUNT)
        if len(destroy_instance_id_list) > bulk_destroy_count:
            raise PermissionDenied(f"You do not have permission to delete more than {bulk_destroy_count} objects")

        # there might be valid ids to which the requested user doesn't have access
        # hence raise the permission denied error
        valid_bulk_instance_qs = self.get_queryset().filter(pk__in=destroy_instance_id_list)
        if len(destroy_instance_id_list) != valid_bulk_instance_qs.count():
            raise PermissionDenied(detail="You do not have permission to delete one or more objects")

        for permission in self.get_permissions():
            if (
                hasattr(permission, "has_bulk_destroy_permission") and
                not permission.has_bulk_destroy_permission(request, self, valid_bulk_instance_qs)
            ):
                raise PermissionDenied(detail="You do not have permission to delete one or more objects")

        # finally, after all checks, perform bulk destroy
        self.perform_bulk_destroy(valid_bulk_instance_qs)

        # return response
        return Response(status=status.HTTP_204_NO_CONTENT)

    def perform_bulk_destroy(self, bulk_instance_qs):
        log.info(f"bulk destroy, instance list: {list(bulk_instance_qs)}")
        # using filtered querset just delete it
        bulk_instance_qs.delete()

This mixin has the view method implementation for the bulk_destroy action which we updated in the custom router mapping previously. Simply, it takes the ids, a list of id of the to-be-deleted objects, does the validation check, permission check and if allowed, then it will remove the requested objects finally.

Now we need this mixin BulkDestroyModelMixin to be inherited in the final viewset. In our Note resource example, it can used like this:

# views.py

from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated

from ..models import Note
from ..serializers import NoteSerializer
from ..permissions import NotePermission
from .mixins import BulkDestroyModelMixin

class NoteViewSet(BulkDestroyModelMixin, viewsets.ModelViewSet):
    """
    NoteViewSet to handle CRUD operations with bulk-destroy.
    """
    queryset = Note.objects.all()
    serializer_class = NoteSerializer
    permission_classes = (IsAuthenticated, NotePermission, )
    allow_bulk_destroy_method = True
    bulk_destroy_count = 5

    def get_queryset(self):
        return Note.objects.filter(owner=self.request.user)

Note that mixin also supports following attributes, which we can define in the viewset if needed:

  • allow_bulk_destroy_method: if viewset can allow bulk destroy
  • bulk_destroy_count: how many objects can be destroyed in a single request

Also, for any reason if a custom permission is needed additionally in the bulk deletion request action, we can define has_bulk_destroy_permission method under the viewset’s permission class. More details on permission here!

Below is the pre-recorded video demo, which exhibits the bulk-destroy action for note resource.

To register users in django shell:

root@17548402114a:/usr/src/app/mysite# python manage.py shell
Python 3.12.7 (main, Oct  1 2024, 22:28:30) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> r = User.objects.create(username="ramesh@example.com", email="ramesh@example.com", first_name="Ramesh")
>>> r.set_password("r#2025")
>>> s = User.objects.create(username="suresh@example.com", email="suresh@example.com", first_name="Suresh")
>>> s.set_password("s#2025")

To generate auth token for users:

root@17548402114a:/usr/src/app/mysite# python manage.py drf_create_token -r ramesh@example.com
Generated token <token> for user ramesh@example.com
root@17548402114a:/usr/src/app/mysite# python manage.py drf_create_token -r suresh@example.com
Generated token <token> for user suresh@example.com

Conclusion

This approach to handle bulk deletion was very helpful to me, in one of the past Django+DRF applications, where the model based viewsets were heavily used. I hope it’d inspire to design and to implement a custom action in DRF.

Thanks for reading the article!


메타데이터
post_id
32fa0249487c
slug
drf-bulk-destroy-in-modelviewset-32fa0249487c
url
https://medium.com/@rsudip90/drf-bulk-destroy-in-modelviewset-32fa0249487c
canonical_url
https://medium.com/@rsudip90/drf-bulk-destroy-in-modelviewset-32fa0249487c
author_url
https://medium.com/@rsudip90
status
ok
fetched_at
2026-07-13 08:05:13