In this article, we'll create a project that posts about rants using Django and Django Rest Framework. We'll use Django's built-in method and override its method to automatically create a for each rant. We'll also use a third-party package called to handle the serialization of a in the model. slugify save() slug drf-writable-nested ManyToManyField We'll start by creating a virtual environment, installing the initial packages, creating the Django project, creating the Django app, and finally doing the initial migrations. python -m venv venv . venv/bin/activate python -m pip install django djangorestframework django-admin startproject myproject cd myproject python -m manage startapp rants python -m manage migrate We list and our rants app in the INSTALLED_APPS settings of our project and include them in the project rest_framework urls.py # settings.py INSTALLED_APPS = [ ... 'rest_framework', 'rants', ] # myproject/urls.py from django.urls import path, include urlpatterns = [ path('', include('rants.urls', namespace='main')), path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), ] Next, we create the models inside the app: rants # models.py from django.db import models class Category(models.Model): title = models.CharField(max_length=50) slug = models.SlugField(max_length=50) def __str__(self): return self.title class Rant(models.Model): title = models.CharField(max_length=150) slug = models.SlugField(max_length=150) categories = models.ManyToManyField( Category, related_name='rants_categories') class Meta: verbose_name = "rant" verbose_name_plural = 'rants' def __str__(self): return self.title We have a Rant model with a title, a slug with a and a categories field with a connected to a Category model with a title and a slug field. CharField ManyToManyField Then we migrate the database . Next, we create a serializer for both models: python -m manage makemigrations && python -m manage migrate # serializers.py from rest_framework import serializers from .models import Rant, Category class CategorySerializer(serializers.ModelSerializer): slug = serializers.SlugField(read_only=True) class Meta: model = Category fields = "__all__" class RantSerializer(serializers.ModelSerializer): categories = CategorySerializer(many=True) slug = serializers.SlugField(read_only=True) class Meta: model = Rant fields = ('id', 'title', 'slug', 'categories') many = True Finally, we create our views and map them to our URLs so we can see the API endpoints of our app. # views.py from rest_framework.response import Response from rest_framework.generics import ListCreateAPIView, UpdateAPIView, DestroyAPIView from .models import Rant from .serializers import RantSerializer class RantList(ListCreateAPIView): queryset = Rant.objects.all() serializer_class = RantSerializer def list(self, request): queryset = self.get_queryset() serializer = RantSerializer(queryset, many=True) return Response(serializer.data) class RantUpdate(UpdateAPIView): queryset = Rant.objects.all() serializer_class = RantSerializer class RantDelete(DestroyAPIView): queryset = Rant.objects.all() serializer_class = RantSerializer We use for read-write endpoints to represent a collection of model instances that provide a and method handler, for update-only endpoints of a single model instance which provides a and method handler, for a delete-only endpoint of a single model instance which provides a method handler. Let's map these views to the ListCreateAPIView get post UpdateAPIView put patch DestroyAPIView delete urls.py # urls.py from django.urls import path from .views import RantList, RantUpdate, RantDelete from .models import Rant from .serializers import RantSerializer app_name = 'rants' urlpatterns = [ path('api/rants/', RantList.as_view(queryset=Rant.objects.all(), serializer_class=RantSerializer)), path('api/rants/update/<int:pk>/', RantUpdate.as_view(queryset=Rant.objects.all(), serializer_class=RantSerializer)), path('api/rants/delete/<int:pk>/', RantDelete.as_view(queryset=Rant.objects.all(), serializer_class=RantSerializer)), ] We can now view the API endpoints in the browser using the drf package, but I personally prefer to see the API endpoints using another package which is the package. Let's install and configure the package: drf-yasg python -m pip install drf-yasg # settings.py INSTALLED_APPS = [ .... 'rest_framework', 'drf_yasg', ] # urls.py from django.urls import path, include from rest_framework import permissions from drf_yasg.views import get_schema_view from drf_yasg import openapi schema_view = get_schema_view( openapi.Info( title="Rants API", default_version='v1', description="Rants", terms_of_service="https://www.google.com/policies/terms/", contact=openapi.Contact(email="contact@snippets.local"), license=openapi.License(name="BSD License"), ), public=True, permission_classes=[permissions.AllowAny], ) urlpatterns = [path('api/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),] Now we run and head over to to see what we've created python -m manage runserver http://localhost:8000/api/ But we have a problem; the categories field has an error despite inputting a in the field. str { "categories": [ "Expected a list of items but got type \"str\"." ] } To solve this, we'll install in our app to serialize the categories field and then update the file to include in our drf-nested-writable serializers.py WritableNestedModelSerializer RantSerializer python -m pip install drf-nested-writable # serializers.py from drf_writable_nested.serializers import WritableNestedModelSerializer class RantSerializer(WritableNestedModelSerializer): categories = CategorySerializer(many=True) slug = serializers.SlugField(read_only=True) class Meta: model = Rant fields = ('id', 'title', 'slug', 'categories') many = True Now when we add new data for our app using the endpoint, we won't get the error we got above anymore but instead. We also override the method in our models for the field to automatically fill the database in rants_create save() slug The code to override the method in our models save() # models.py ... from django.utils.text import slugify ... def save(self, *args, **kwargs): self.slug = slugify(self.title) super(Category, self).save(*args, **kwargs) return self.slug ... def save(self, *args, **kwargs): self.slug = slugify(self.title) super(Rant, self).save(*args, **kwargs) return self.slug ... This took me a while to solve since I'm at GMT+8, but hey, at least it got solved, and I learned how to override the save method in the models and learned about the drf-writable-nested package to fix my issue. Conclusion: Also published . here