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→ createGET→ readPUTorPATCH→ updateDELETE→ 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.titleThis 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 migrate3. 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:
GETuses it to convert objects into JSON-ready dataPOSTuses it to validate and create new objectsPUTandPATCHuse 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:
ListCreateAPIViewRetrieveUpdateDestroyAPIView
These names are very descriptive:
ListCreateAPIViewhandles listing many objects and creating new onesRetrieveUpdateDestroyAPIViewhandles 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 = BookSerializerThis 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:
ListCreateAPIViewcombines list behavior and create behaviorRetrieveUpdateDestroyAPIViewcombines 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.
PUTis generally used for a full updatePATCHis used for a partial update
So if your object has five fields and you send only one changed field:
PATCHis usually the correct methodPUTis 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 = BookSerializerThat 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 valueThis 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 = BookSerializerThis 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.titleserializers.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 valueviews.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 = BookSerializerbooks/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.
💬 Comments
No comments yet. Be the first to comment!
Login to comment.