After building your first API, the next major step is learning how to create a complete CRUD API. CRUD means Create, Read, Update, Delete. In practice, this means your API can add new records, return existing records, modify them, and remove them. Django REST Framework is especially strong here because it provides multiple ways to implement CRUD, from explicit APIView classes to higher-level generic views and even ViewSet + router patterns. The official DRF documentation describes generic views as pre-built views for common patterns that map closely to database models, and it describes viewsets as a way to combine related view logic into a single class.

In this tutorial, we will build a full CRUD API for a Book model and understand it at a deep level. We will start from the concept of CRUD, then implement it with DRF generic views, and finally see how the same API can become even cleaner with ModelViewSet and routers. This is an important stage in learning DRF because it is where your API starts to feel like a real application backend rather than a simple experiment. DRF’s own tutorial sequence moves from class-based views to more reusable generic views and then to viewsets and routers, which reflects the natural progression most learners should follow.

1. What CRUD means in an API

CRUD stands for:

  • Create → add a new object
  • Read → retrieve one or more objects
  • Update → modify an existing object
  • Delete → remove an object

In REST-style APIs, these actions usually correspond to HTTP methods:

  • POST → create
  • GET → read
  • PUT or PATCH → update
  • DELETE → delete

DRF is designed around these patterns. Its generic views include classes such as ListCreateAPIView and RetrieveUpdateDestroyAPIView, which directly match common CRUD needs.

So when people say “build a CRUD API,” they usually mean “build a resource-oriented API with endpoints that support the full lifecycle of an object.”

2. The model we will use

We will continue using a simple Book model. This is enough to demonstrate all CRUD operations clearly.

models.py

from django.db import models

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=120)
    published_year = models.IntegerField()
    description = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

This is still standard Django. DRF works on top of Django, not instead of it. The model defines the database structure, while DRF will help us expose it as an API. The DRF homepage and quickstart both assume the normal Django project workflow: define your app and data model, then expose it through serializers and API views.

After creating or updating the model, run:

python manage.py makemigrations
python manage.py migrate

3. Creating the serializer

A serializer is the bridge between your Django model and the API output/input. The official serializer guide explains that serializers transform model instances and querysets into native Python data suitable for rendering into JSON, and they also deserialize and validate incoming data. It also explains that ModelSerializer is a shortcut for working with model instances and querysets.

serializers.py

from rest_framework import serializers
from .models import Book

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'published_year', 'description', 'created_at']

This serializer defines which fields the API will expose. It also gives you validation automatically based on the model field types.

Why the serializer is central in CRUD

Every CRUD operation passes through the serializer in one way or another:

  • GET uses it to convert objects into JSON-ready data
  • POST uses it to validate and create new objects
  • PUT and PATCH use it to validate and update objects
  • Validation errors are returned through it in a structured way

This is why serializers are one of the most important parts of DRF.

4. Building CRUD with generic views

When beginners first learn DRF, they often write everything manually with APIView. That is useful for understanding the mechanics, but DRF’s generic views are the next big improvement. The generic views documentation says they allow you to quickly build API views that map closely to your database models, and that they are built by combining GenericAPIView with reusable mixin classes.

For a CRUD API, two generic views are especially useful:

  • ListCreateAPIView
  • RetrieveUpdateDestroyAPIView

These names are very descriptive:

  • ListCreateAPIView handles listing many objects and creating new ones
  • RetrieveUpdateDestroyAPIView handles retrieving one object, updating it, and deleting it

views.py

from rest_framework import generics
from .models import Book
from .serializers import BookSerializer

class BookListCreateAPIView(generics.ListCreateAPIView):
    queryset = Book.objects.all().order_by('-created_at')
    serializer_class = BookSerializer

class BookDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

This is a remarkably small amount of code for a full CRUD API. DRF provides the behavior, and you configure it by setting the queryset and serializer. The generic views API guide explicitly lists both ListCreateAPIView and RetrieveUpdateDestroyAPIView among the built-in concrete generic views.

5. Why this works

These generic views work because DRF composes them from reusable behaviors. The documentation explains that each concrete generic view is built from GenericAPIView plus one or more mixins. That design is important because it means DRF is not magic; it is structured reusable class composition.

Conceptually:

  • ListCreateAPIView combines list behavior and create behavior
  • RetrieveUpdateDestroyAPIView combines retrieve, update, and destroy behavior

This means DRF already knows how to:

  • fetch the queryset
  • serialize the results
  • validate incoming data
  • save new or updated objects
  • return standard responses
  • handle object lookup by primary key

That is why CRUD becomes so concise with generic views.

6. Connecting the URLs

Now let us expose these views through URLs.

books/urls.py

from django.urls import path
from .views import BookListCreateAPIView, BookDetailAPIView

urlpatterns = [
    path('books/', BookListCreateAPIView.as_view(), name='book-list-create'),
    path('books/<int:pk>/', BookDetailAPIView.as_view(), name='book-detail'),
]

Then include them in your project’s main urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('books.urls')),
]

Now your CRUD API endpoints are:

  • GET /api/books/
  • POST /api/books/
  • GET /api/books/<id>/
  • PUT /api/books/<id>/
  • PATCH /api/books/<id>/
  • DELETE /api/books/<id>/

This is now a complete CRUD API.

7. Understanding each CRUD operation deeply

Create: POST /api/books/

This creates a new book. The request sends JSON data such as:

{
  "title": "Mastering DRF",
  "author": "Jane Smith",
  "published_year": 2025,
  "description": "A complete guide to Django REST Framework."
}

The serializer validates that data. If valid, the object is saved. If invalid, DRF returns a structured 400 Bad Request response.

This matters because API clients should never have to guess what went wrong. DRF makes validation feedback readable and standardized.

Read: GET /api/books/

This returns all books, usually as a JSON list.

Read one: GET /api/books/1/

This returns a single book object.

Update: PUT /api/books/1/

This replaces the object data with the submitted payload.

Partial update: PATCH /api/books/1/

This modifies only the provided fields.

Delete: DELETE /api/books/1/

This removes the object and usually returns a 204 No Content response.

These are the core behaviors of CRUD APIs, and DRF’s generic views are designed exactly for them.

8. PUT vs PATCH

This is a very important concept in CRUD APIs.

  • PUT is generally used for a full update
  • PATCH is used for a partial update

So if your object has five fields and you send only one changed field:

  • PATCH is usually the correct method
  • PUT is more often treated as replacing the full representation

DRF supports both update styles in its update-capable views. In practical API design, PATCH is very common because clients often change only one or two fields at a time. This fits naturally with DRF’s update handling in generic views and viewsets.

9. Why generic views are better than writing everything manually

You could build CRUD using APIView with explicit get, post, put, patch, and delete methods. That is fine for learning, but it becomes repetitive. DRF’s generic views exist specifically to remove that repetition. The official docs explain that they were designed to abstract common patterns so you can build common data views quickly without repeating yourself.

So instead of writing lots of manual logic, you write:

class BookListCreateAPIView(generics.ListCreateAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

That is one of the biggest productivity wins in DRF.

10. Customizing CRUD behavior

Even though generic views are compact, they are still flexible. The generic views guide notes that in more complex cases you can override methods and customize behavior.

For example, if you want to automatically set the current user as the creator of a book record, you can override perform_create():

from rest_framework import generics
from .models import Book
from .serializers import BookSerializer

class BookListCreateAPIView(generics.ListCreateAPIView):
    queryset = Book.objects.all().order_by('-created_at')
    serializer_class = BookSerializer

    def perform_create(self, serializer):
        serializer.save()

If your model had a user field, you might do:

def perform_create(self, serializer):
    serializer.save(user=self.request.user)

This is a powerful idea: generic views give you standard CRUD behavior, but you can still customize important steps.

11. Adding validation rules

CRUD APIs become much more useful when they enforce business rules.

For example, you may want to reject books with impossible publication years. You can add serializer validation:

from rest_framework import serializers
from .models import Book
from datetime import datetime

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'published_year', 'description', 'created_at']

    def validate_published_year(self, value):
        current_year = datetime.now().year
        if value < 1900 or value > current_year:
            raise serializers.ValidationError("Please enter a valid publication year.")
        return value

This keeps your CRUD API trustworthy. It does not merely accept input; it protects the quality of the data.

12. Using the browsable API for CRUD testing

One of DRF’s best features is the browsable API. The official browsable API documentation explains that DRF can generate human-friendly HTML pages for resources and provide forms for submitting POST, PUT, and DELETE requests. It also notes you can force JSON output using ?format=json.

That means you can test your CRUD API directly in the browser:

  • list books
  • create a book
  • edit a book
  • delete a book
  • inspect JSON output

For a beginner, this is much easier than immediately relying on frontend code or external API clients.

13. Going one step further: CRUD with ModelViewSet

Once you understand CRUD with generic views, the next abstraction is the viewset. The viewsets guide explains that a ViewSet combines the logic for related views in a single class, and instead of defining methods like .get() and .post(), it provides actions such as .list() and .create(). It also explains that routers can automatically generate URL patterns for viewsets.

Here is the same CRUD API using a ModelViewSet.

views.py

from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all().order_by('-created_at')
    serializer_class = BookSerializer

This one class can handle full CRUD behavior.

urls.py

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import BookViewSet

router = DefaultRouter()
router.register(r'books', BookViewSet, basename='book')

urlpatterns = [
    path('', include(router.urls)),
]

This is much shorter than writing individual URL patterns manually. The DRF quickstart explicitly highlights that when using viewsets, routers can automatically generate the URL configuration.

14. Generic views vs viewsets: which should you choose?

This is a very common question.

Use generic views when:

  • you want clear, explicit structure
  • you are still learning DRF
  • you want more visible control over list/create vs detail/update/delete views

Use viewsets when:

  • you want to reduce repeated code further
  • your API follows standard CRUD resource patterns
  • you want routers to handle URL generation automatically

DRF supports both because different projects need different levels of explicitness. The docs note that if generic views do not suit your needs, you can drop down to APIView, and if you want more abstraction, you can use viewsets and routers.

For learning, generic views are usually the best middle ground.

15. Common mistakes in CRUD APIs

A very common mistake is forgetting the difference between list endpoints and detail endpoints. /books/ is not the same as /books/<id>/.

Another mistake is not understanding the serializer’s role. The serializer is not just a formatter; it is also your validation layer.

Another one is using PUT when you really want PATCH, especially when only a small part of an object changes.

Some beginners also jump immediately into ModelViewSet without understanding what CRUD operations are happening underneath. That often works at first, but it can make debugging harder later.

Another practical mistake is forgetting to expose authentication routes for the browsable API when authentication is involved. The DRF homepage notes that, if you intend to use the browsable API, adding REST framework’s login and logout views is useful, and the quickstart includes api-auth/ for that purpose.

16. Full CRUD example recap with generic views

models.py

from django.db import models

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=120)
    published_year = models.IntegerField()
    description = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

serializers.py

from rest_framework import serializers
from .models import Book
from datetime import datetime

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'published_year', 'description', 'created_at']

    def validate_published_year(self, value):
        current_year = datetime.now().year
        if value < 1900 or value > current_year:
            raise serializers.ValidationError("Please enter a valid publication year.")
        return value

views.py

from rest_framework import generics
from .models import Book
from .serializers import BookSerializer

class BookListCreateAPIView(generics.ListCreateAPIView):
    queryset = Book.objects.all().order_by('-created_at')
    serializer_class = BookSerializer

class BookDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

books/urls.py

from django.urls import path
from .views import BookListCreateAPIView, BookDetailAPIView

urlpatterns = [
    path('books/', BookListCreateAPIView.as_view(), name='book-list-create'),
    path('books/<int:pk>/', BookDetailAPIView.as_view(), name='book-detail'),
]

Project urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('books.urls')),
    path('api-auth/', include('rest_framework.urls')),
]

That last api-auth/ route is optional, but it is useful for the browsable API login/logout flow.

17. Why CRUD APIs matter so much in real projects

CRUD is not just an academic exercise. Most real-world backend systems include CRUD operations somewhere:

  • blog post management
  • products in e-commerce
  • user profiles
  • course lessons
  • comments
  • support tickets
  • orders
  • categories
  • dashboards and admin tools

When you learn CRUD well in DRF, you are learning one of the most reusable backend patterns in modern application development. Generic views and viewsets make that pattern much faster to implement, and DRF’s browsable API makes it easier to test and debug during development.

Conclusion

A CRUD API in Django REST Framework is the natural next step after your first simple API because it teaches you how to manage the full lifecycle of data. DRF’s official documentation presents generic views like ListCreateAPIView and RetrieveUpdateDestroyAPIView as shortcuts for common model-based API patterns, and it presents viewsets and routers as an even more compact way to organize related CRUD logic. Together, these tools let you build practical APIs quickly while still keeping the code clean and maintainable.

The most important thing to remember is this: CRUD in DRF is really about connecting models, serializers, views, and URLs into a structured data-management pipeline. Once that clicks, you can build far more advanced APIs with confidence.